#ifdef TEST

Dzisiejszy wpis poświęcony będzie tematyce, którą powinienem był poruszyć już ładnych parę tygodni temu – testowaniu warstw dostępu do danych i logiki biznesowej. Zwlekałem z opisaniem tej części projektu, ponieważ planowałem zaprezentować również testy interfejsu użytkownika, których, koniec końców, wcale nie napisałem (pokusa oglądania aplikacji w akcji i sprawdzania wszystkiego własnoręcznie okazała się zbyt duża).
Przejdźmy jednak do rzeczy. Jak już kiedyś wspominałem, solucja InvoiceInvoker składa się z trzech projektów:

  • InvoiceInvoker.Logic – Data Access Layer + Business Logic Layer, z perspektywy czasu stwierdzam, że rozdzielenie tych warstw nie byłoby złym pomysłem,
  • InvoiceInvoker.Logic.Tests – testy jednostkowe pierwszego projektu,
  • InvoiceInvoker.MvcUi – projekt ASP.NET MVC 2, realizujący interakcję z użytkownikiem.

Bohaterem niniejszego wpisu będzie drugi z wymienionych projektów. Zanim przybliżę jego zawartość, opiszę framework testów jednostkowych, z którego korzystam:

NUnit
Testy w NUnit pisze się tak samo, jak zwykłe klasy. Muszą to być klasy publiczne i opatrzone atrybutem TestFixture. Idąc za ciosem, metody testujące to zwykłe metody publiczne z atrybutem Test, zwracające typ void. Przykładowy test klasy wykonującej obliczenia (nazwijmy ją Calculator) wyglądałby więc tak:

using System;
using NUnit.Framework; // przestrzeń nazw NUnit, znajdująca się w bibliotece nunit.framework

namespace SomeProject.Tests
{
	[TestFixture]
	public class CalculatorTests
	{
		[Test]
		public void Adds()
		{
			Calculator calculator = new Calculator(); // utworzenie obiektu testowanej klasy
			int a = 3;
			int b = 5;
			int actual = calculator.Add(a, b); // wynik testowanej metody
			int expected = 8; // spodziewany wynik

			Assert // NUnit.Framework.Assert - klasa, której metody zwracają wynik testu
				.AreEqual( // wynik testu będzie pozytywny, jeśli argumenty będą równe
					expected, actual); // pierwszym argumentem jest spodziewany wynik testowanej metody, drugim - wynik rzeczywisty
		}
	}
}

Gdybyśmy chcieli wykonać ten sam test dla róznych par liczb (nie tylko 3 i 5), metoda Adds mogłaby wyglądać tak:

[Test, Sequential] // Sequential - test zostanie wykonany wielokrotnie
public void Adds([Values(0, 3, -3, 3)] int a, // 0, 3, -3, 3 - kolejne argumenty wywołania testu
	[Values(0, 5, 3, -5)] int b, [Values(0, 8, 0, -2)] int expected)
{
	Calculator calculator = new Calculator();
	int actual = calculator.Add(a, b);

	Assert.AreEqual(expected, actual);
}

Łatwo zauważyć, że „zaszarzona” linia będzie się pojawiać w każdym tego typu teście (Adds, Substracts, Multiplies itp.). Dla takich przypadków istnieje atrybut SetUp:

Calculator calculator; // pole prywatne

[SetUp] // atrybut sprawia, że poniższa metoda zostanie wywołana przed każdym testem
public void TestsSetup()
{
	calculator = new Calculator(); // tę linę można już usunąć z treści metod testujących
}

Może się zdarzyć, że w testach będziemy odwoływać się do pliku konfiguracyjnego testowanego projektu. Warto wiedzieć, że NUnit domyślnie korzysta z pliku konfiguracyjnego [NazwaSolucji].config, mającego się znajdować w katalogu solucji. Odkrycie tej drobnostki zajęło mi dobrą godzinę, wypełnioną zachodzeniem w głowę, dlaczego wynik testu połączenia z bazą danych jest pozytywny, kiedy connection string umieszczony jest „na sztywno” w klasie repozytorium, a negatywny – kiedy jest pobierany z pliku konfiguracyjnego (znajdującego się katalaogu projektu, a nie solucji).

Testy warstwy dostępu do danych
Po przedstawieniu działania NUnit, mogę zaprezentować sposób testowania poszczególnych elementów projektu InvoiceInvoker.Logic. Zacznę od testów warstwy DAL, które umieściłem w przestrzeni nazw InvoiceInvoker.Logic.Tests.RepositoriesTests. Przyznam szczerze, że sposób ich wykonania budzi wiele wątpliwości. Po pierwsze, operacje Get wykonywane są zawsze na encji o identyfikatorze 1, a Update – 2:
(przykład testowania repozytorium sprzedawców – RegisteredSellerRepository)

[Test]
public void GetsById()
{
	RegisteredSeller result = repository.GetById(1); // repository - prywatne pole typu RegisteredSellerRepository

	// sprzedawca o identyfikatorze 1 ma dane takie, jak te podane jako pierwsze argumenty poniższych wywołań:
	Assert.AreEqual("BankAccountNumber", result.BankAccountNumber);
	Assert.AreEqual("BankName", result.BankName);
	Assert.AreEqual("BankSwift", result.BankSwift);
	Assert.AreEqual("City", result.City);
	Assert.AreEqual("CompanyName", result.CompanyName);
	Assert.AreEqual("FirstName", result.FirstName);
	Assert.AreEqual(1, result.Id);
	Assert.AreEqual("InvoiceNumberFormat", result.InvoiceNumberFormat);
	Assert.AreEqual("LastInvoiceNumber", result.LastInvoiceNumber);
	Assert.AreEqual("LastName", result.LastName);
	Assert.AreEqual("NIP", result.Nip);
	Assert.AreEqual("PostalCode", result.PostalCode);
	Assert.AreEqual("REGON", result.Regon);
	Assert.AreEqual("Street", result.Street);
	Assert.AreEqual("UserName", result.UserName);
}

Jeśli dane sprzedawcy o identyfikatorze 1 ulegną zmianie (musiałbym je zmienić ręcznie, jednak istnieje taka możliwość), to test będzie miał wynik negatywny, nawet jeśli metoda GetsById zadziała poprawnie. Test Updates jest wolny od tej wady, ponieważ pobiera encję z bazy, zmienia jej dane, wywołuje metodę Update, a następnie pobiera ją znowu i sprawdza, czy dane się zmieniły. Jednak pobranie encji wiąże się z wywołaniem metody GetById, która nie jest przedmiotem testu – taki test trudno nazwać jednostkowym. Wątpliwości innej natury budzi test dodawania encji – bada tylko, czy liczba wierszy tabeli po wywołaniu metody Add zwiększa się o jeden. Test usuwania łączy dwie ostatnie wady: polega na dodaniu encji (a więc zakłada, że ta operacja działa), usunięciu jej, i sprawdzeniu, czy ilość encji w tabeli nie uległa zmianie.
Czy za napisanie takich testów zasługuję na wieczne potępienie? Jeśli tak, to proszę o niezwłoczne powiadomienie mnie o tym w komentarzu – udam się na wieczną tułaczkę. Na swoją obronę mam tylko tyle, że testy te rzeczywiście pomogły mi wykryć kilka błędów.

Testy generowania dokumentów PDF
Tutaj krótko: nie miałem pojęcia, jak za pomocą kodu sprawdzić, czy wygenerowany plik PDF wygląda jak faktura VAT. Testy polegają więc na wygenerowaniu faktury o określonej właściwości (na przykład zawierającej bardzo długie nazwy produktów) i wyświetleniu jej. Wyniki takich testów zamieściłem na końcu tego wpisu.

Testy logiki biznesowej
Nareszcie testy wolne od kontrowersji. Dotyczą one prawie wszystkich klas opisywanych we wpisach otagowanych jako business logic layer. Dla przykładu, testy klasy zamieniającej liczby na ich reprezentację słowną:

[Test]
public void ConvertsNumbersOf4Digits()
{
	string result = NumberToWordsConverter.Convert(1234);
	Assert.AreEqual("jeden tysiąc dwieście trzydzieści cztery", result);
}

[Test]
public void ConvertsNumbersOf5Digits()
{
	string result = NumberToWordsConverter.Convert(12345);
	Assert.AreEqual("dwanaście tysięcy trzysta czterdzieści pięć", result);
}

// (...)

[Test]
public void ConvertsMoney()
{
	string result = NumberToWordsConverter.ConvertMoney(12.93M, "PLN");
	Assert.AreEqual("dwanaście PLN 93/100", result);

To, co nie budzi kontrowersji, nie budzi też zainteresowania (nie jest to przypadkiem jakieś prawo show-biznesu?) – dlatego pominę szczegółowy opis, dociekliwych odsyłając do kodu źródłowego.

I to by było na tyle. Ten (najdłuższy chyba ze wszystkich) wpis kończy serię wpisów „technicznych”. Zakończenie konkursu zbliża się wielkimi krokami, dlatego pozostałe dwie (bo tyle brakuje do wymaganych dwudziestu) notki poświęcę na podsumowanie tej kilkunastotygodniowej przygody. Zapraszam!