W tej części cyklu stworzymy nasz pierwszy test jednostkowy. Przedstawię krok po kroku jak napisać i przetestować prostą funkcjonalność wedle zasad TDD. Opiszę tutaj szczegółowo wszystkie kroki, począwszy od tego jak dodać referencję do NUnita, a skończywszy na tym jak uruchomić test. Zachęcam też do odwiedzenia ostatniej części, która opisuje strukturę kodu jednostkowego Arrange-Act-Assert “Kurs TDD część 3: Struktura testu, czyli Act-Arrange-Assert”).
Zachęcam tym bardziej, że rozszerzyłem tę część o garść istotnych informacji. Za przykład posłuży nam klasa kalkulatora i metoda służąca do dodawania dwóch liczb. Przyjmijmy, że wymagania precyzują, że metoda ta przyjmuje dwa parametry wejściowe typu int i zwraca wartość też typu int. Nasza funkcjonalność wygląda w kodzie następująco:
public class Calculator
{
public int Add(int a, int b)
{
throw new NotImplementedException();
}
}
Pamiętamy, że w Test-Driven Development testy piszemy w kroku pierwszym; dopiero później implementujemy kod i refaktoryzujemy.
Pierwszą czynnością po uruchomieniu Visual Studio jest utworzenie nowej solucji, dodanie projektów i zalążków naszych klas:
Calculator
.Class1.cs
zastępujemy klasą Calculator.cs
i wklepujemy metodę Add, dokładnie taką jak powyżej.Calculator.Tests
.Calculator.Tests
klasę o nazwie CalculatorTests
.Calculator.Tests
. (prawy klik na projekt Calculator.Tests > Add Reference… > Projects > ✅ Calculator)Zwróć uwagę na nazewnictwo projektu i klas. Nieformalna zasada mówi, że projekt z testami powinien mieć przyrostek .Tests, natomiast klasa zawierająca testy przyrostek Tests. Jeśli wszystko poszło dobrze, całość powinna wyglądać tak:
Kolejnym krokiem jest dodanie biblioteki NUnit do naszego projektu. Możemy to zrobić za pomocą NuGet. Jeśli NuGet jest Ci obcy, polecam mimo wszystko ten krok, gdyż jest bajecznie łatwy i sporo szybszy.
Nie będę opisywać ręcznego dodawania biblioteki, gdyż jest to nie rekomendowany sposób pracy z bibliotekami zewnętrznymi
Komentarz: Pamiętaj, że prócz NUnita są też inne frameworki do testowania, np. MSTest dostarczany z Visual Studio lub xUnit.NET. NUnit jest jednak najpopularniejszy w swojej dziedzinie.
Calculator.Tests
.Alternatywnie, jeśli nie chcemy instalować biblioteki z linii komend, a wolimy UI, to w oknie Solution Explorer można kliknąć prawym przyciskiem myszy na opcję Manage NuGet Packages i zainstalować NUnita z zakładki Browse.
Jeśli nie posiadamy ReSharpera, to należy dodać do projektu bibliotekę NUnit3TestAdapter.
Pierwszym etapem cyklu red-green-refactor jest faza red, czyli napisanie testów do jeszcze nieistniejącej funkcjonalności. Przed napisaniem testów jednostkowych musimy zastanowić się m.in. nad wszystkimi sytuacjami wyjątkowymi oraz wartościami brzegowymi. Nasz przypadek dodawania jest ekstremalnie łatwy. Nie oczekujemy wystąpienia wyjątku, nie mamy do czynienia z wartościami NULL ani pustymi stringami, nie mamy też zdarzeń, typów generycznych ani zależności do innych klas. Jakie przypadki możemy więc sprawdzić? Jako, że dodajemy do siebie dwie liczby typu int, możemy zrobić testy jednostkowe do następujących przypadków:
Taki zestaw testów zostanie przygotowany do naszej metody Add
. Jeśli wszystkie testy przejdą pozytywnie, uznamy że nasza funkcjonalność napisana jest poprawnie. Każdy test jednostkowy napisany w NUnit z punktu widzenia technicznego to publiczna metoda void z atrybutem [Test]
.
I tyle! Bierzemy się zatem do napisania pierwszego testu jednostkowego. Jeszcze jedna kwestia – jaka jest konwencja nazewnictwa w świecie TDD? Jest wiele szkół nazewnictwa, najważniejsze jednak żeby nazwa testu zawierała w sobie informacje:
Przykładowo, do parametrów metody CheckPassword, wprowadzamy poprawną nazwę użytkownika i hasło; oczekujemy, że metoda wróci w takim przypadku wartość true. Dla takiego przypadku nazwa testu jednostkowego może być następująca: CheckPassword\ValidUserAndPassword_ReturnTrue_. Spójrzmy na początek tego rozdziału i cztery przypadki do naszej metody. Pamiętając o tym, że jeden test jednostkowy testuje jedną funkcjonalność, musimy stworzyć cztery testy jednostkowe, a co za tym idzie cztery metody. Reasumując całą dotychczasową wiedzę z naszego kursu, pierwszy test, test dwóch liczb dodatnich, będzie wyglądać następująco:
[Test]
public void Add_AddsTwoPositiveNumbers_Calculated()
{
var calc = new Calculator.Calculator();
int sum = calc.Add(2, 2);
Assert.AreEqual(4, sum);
}
Nazwa naszej klasy mówi nam o tym:
W powyższym przykładzie inicjalizujemy obiekt klasy Calculator
. Następnie dodajemy dwie liczby i przypisujemy rezultat do zmiennej o nazwie sum
. Najważniejszym krokiem jest jednak ostatnia linijka, w której sprawdzamy czy wartość tej sumy odpowiada wartości oczekiwanej.
Kolejne trzy testy jednostkowe możemy “napisać metodą copy-paste”, zmieniając nazwę testu, parametry wejściowe metody oraz wartość oczekiwaną. Zaraz, ale co z czystym kodem (możemy przypisać wcześniej wartości zmiennych, np. int expected = 4
), duplikacją kodu (możemy wyrzucić zmienną calc
do składowych klasy), itd.?
Otóż świat TDD ma trochę inne prawa niż kod produkcyjny. Dozwolone jest nieprzypisywanie zmiennych (testujemy czy 2 + 2 = 4, nie potrzeba nam więc przypisywać te wartości do zmiennych). Testy jednostkowe powinny być atomiczne i zawierać jak najmniej elementów mogących spowodować bugi w… tak, tak — teście; dozwolona jest więc w naszym teście jednostkowym duplikacja kodu, unikać powinno się takich elementów jak instrukcje warunkowe, pętle, dziedziczenie. Oczywiście, możemy znaleźć następstwa od tej reguły, takie jak obszerny setup metody z wieloma zależnościami, niemniej jednak należy trzymać się ww. reguł w codziennym życiu z testami.
Mamy zatem cztery metody z niemal identycznym kodem, dlaczego więc nie użyć jednej metody Add_AddsTwoNumbers_Calculated
i zawrzeć w niej czterech asertów, jeden za drugim?:
Assert.AreEqual(4, calc.Add(2, 2));
Assert.AreEqual(-1, calc.Add(2, -3));
Assert.AreEqual(1, calc.Add(-2, 3));
Assert.AreEqual(-4, calc.Add(-2, -2));
Takie rozwiązanie ma bardzo dużą wadę: Jeśli którakolwiek z tych asercji nie zostanie spełniona, cały test jednostkowy będzie czerwony. O ile NUnit pozwala sprawdzić, która asercja nie została spełniona i dlaczego, nam nie zależy na informacji która asercja została niespełniona, ale czy cały test się powiódł lub nie. Innymi słowy, musimy na podstawie nazwy testu wywnioskować, co poszło nie tak w naszym teście. Testując wiele elementów w jednym teście, nie jesteśmy jednoznacznie stwierdzić co poszło nie tak. Stąd też kluczowa reguła TDD mówiąca o tym, że powinniśmy testować zawsze jedną rzecz. Bardzo ważne są też nazwy naszych testów, przybierające z tych powodów niekiedy kosmicznie długie rozmiary.
Testy możemy uruchomić wybierając z menu Visual Studio opcję Test > Run > All Tests (warunkiem jest posiadanie NUnit3TestAdapter dołączonego do naszego projektu).
Jeśli mamy ReSharpera, możemy to zrobić na dwa różne sposoby:
Sposób 1.: Klikamy prawym przyciskiem myszy na projekt Calculator.Tests
i wybieramy Run Unit Tests, lub
Sposób 2.: Klikamy w edytorze kodu na zielonożółte kółko przy teście i wybieramy Run.
Po uruchomieniu testów wszystkie testy będą czerwone:
W etapie green piszemy wreszcie nasz kod. W naszym przypadku rocket science to nie jest, do naszej metody Add
wrzucamy
return a + b;
Po uruchomieniu testów, wszystkie testy zaświeciły się na zielono:
Nasz przykład jest tak prosty, że nie potrzebujemy nic ulepszać. Pamiętajmy jedynie, że ten etap jest tak samo ważny co dwa pozostałe. Po dokończonej refaktoryzacji, odpalamy na nowo wszystkie testy i sprawdzamy czy nasza refaktoryzacja nie wprowadziła błędu. Nie dotyczy to naszego przypadku, gdyż nic nie refaktoryzowaliśmy.
W podanym przykładzie chciałem skupić się głównie na pierwszym zetknięciu z NUnitem. Całość opisałem w najprostszych krokach w taki sposób, aby osoba początkująca z Visual Studio czy C# mogła swobodnie napisać kod wg TDD i uruchomić testy. Z tego względu wybrałem bardzo prosty przypadek – dodawanie dwóch liczb całkowitych.
W następnej części przejdziemy do testowania innej funkcjonalności – dzielenia. Dzięki temu przyjrzymy się trochę innym aspektom TDD. Będziemy musieli przyjrzeć się przypadkowi dzielenia przez zero (oczekujemy na wystąpienie wyjątku), jak i operacjom zmiennoprzecinkowym. Zachęcam do zrobienia takiego przykładu w domu! :)
Część I: Testy jednostkowe – wstęp