Lepiej zapobiegać, czyli podstawy testów jednostkowych
Z tego tekstu dowiesz się jak i po co stosować testy jednostkowe. Tu są same podstawy. Bardziej zaawansowany tekst jest dostępny tutaj.
Co to takiego?
Testowanie jednostkowe (ang. unit testing) polega na automatycznym testowaniu Twojego kodu. Automatyczne – czyli robi to za Ciebie framework. Jest kilka popularnych frameworków testujących dla .NETu. Wszystkie one współpracują z VisualStudio.
Na czym to polega?
Polega to na sprawdzeniu Twojej metody mniej więcej na takie sposoby:
- czy kod się nie wywala lub wywala się wtedy, gdy powinien
- czy kod zachowa się dobrze z różnymi parametrami (parametry brzegowe i „zwyczajne”)
- czy kod zwraca poprawne dane
- jak kod zachowa się w specyficznych warunkach
Test jednostkowy polega z grubsza na napisaniu specjalnego kodu, który testuje inny kod. O tym za chwilę.
Testowanie jednostkowe pomaga
Testy jednostkowe pomagają… bardzo pomagają uniknąć błędów w kodzie produkcyjnym. Generalnie bardzo potrafią ograniczyć występowanie błędów. Testy jednostkowe są najbardziej użyteczne w momencie, gdy refaktorujemy kod lub robimy zmiany w już działającym.
Zapewne nie raz przekonałeś się, że poprawki zrobione w jednej części kodu tworzą błędy zupełnie gdzieś indziej, prawda? Jest to swego rodzaju zmora. Ale cóż… jako ludzie nie jesteśmy w stanie wszystkiego przewidzieć. I tutaj bardzo pomocne okazują się być testy jednostkowe.
Testy dokumentują kod
Testy jednostkowe „dokumentują” też w pewien sposób kod. To nie znaczy, że nie powinno prowadzić się prawdziwej dokumentacji 😉 W jaki sposób test dokumentuje? Po prostu widzisz fragment poprawnie napisanego kodu – np. użycie jakiejś klasy – w odpowiedni sposób – w taki jaki została zaprojektowana. Dzięki temu wiesz, jak taką konkretną klasę utworzyć i jak jej używać. Przeglądając zatem kody testów możesz się sporo dowiedzieć o całym systemie.
Jak testować?
Pokażę Ci to na przykładzie frameworka nUnit. Dlaczego ten? Po prostu z własnego przyzwyczajenia. Są inne, MSTest, xUnit (te dwa są na dzień dobry w VisualStudio) i sporo innych. Generalnie zasada jest ta sama. Różnice są jeśli chodzi o kod testujący, inicjujący itp.
Przygotowanie
Zainstaluj w Visual Studio następujące rozszerzenia: NUnit 3 Test Adapter i NUnit VS Templates. NUnit TestAdapter to jest rozszerzenie, które współpracuje z TestExplorerem w Visual Studio (takie specjalne okno z widocznymi testami i ich wynikami). NUnit VS Templates natomiast to szablony, dzięki którym możesz tworzyć od razu nowe projekty testujące.
Potrzebujemy teraz jakiegoś programu do testowania. Więc zróbmy coś małego i prostego. Utwórz sobie projekt Class Library (.NET Standard) – dlatego taki, ponieważ chcę uniknąć na tym poziomie aplikacji okienkowych lub konsolowych. Skupimy się tylko na testach i samej logice.
Stwórz prostą klasę Calculator:
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
public int Mul(int x, int y)
{
return x * y;
}
}
Sytuacja wygląda tak – mamy publiczną klasę i dwie publiczne metody. To są idealni kandydaci, żeby zastosować na nich testy jednostkowe.
Żeby mieć porządek w solucji, utwórz teraz w niej katalog Tests. W tym katalogu utwórz nowy projekt: nUnit Test Project (.NET Core) -> nie znajdziesz go, jeśli nie zainstalowałeś NUnit VS Templates.
Interesuje nas ten konkretnie projekt, ponieważ będziemy testować bibliotekę .NETStandard. Gdybyśmy testowali aplikację na Androida, wybralibyśmy NUnit 3 Test Project (Android). Analogicznie z innymi szablonami, które widzisz w oknie dodawania nowego projektu.
Możesz nazwać swój projekt, np: NUnit.Nazwa-Projektu-Ktory-Testujesz. Oczywiście możesz nazwać go dowolnie, ale takie nazewnictwo wydaje się być rozsądne, bo:
- pokazuje Ci jakim frameworkiem testujesz
- pokazuje Ci jaki projekt testujesz
Osobiście bardziej używam standardowego nazewnictwa w stylu: NazwaFirmy.NazwaAplikacji.ProjektTests
(gdzie Projekt
, to nazwa projektu, który testuję)
Pierwszy test
OK, zróbmy zatem pierwsze testy jednostkowe. Ja zazwyczaj robię tak (i Tobie też polecam), że jedną klasę testuję w jednym pliku. Zatem w projekcie testowym utwórz sobie plik C# o nazwie CalculatorTests.cs
Od razu do pliku dodaj dyrektywę using NUnit.Framework
I teraz tak – całe testowanie w nUnit opiera się głównie na atrybutach. Żeby Twoja klasa była uznana za klasę z testami, musisz ją oznaczyć atrybutem TestFixture
:
[TestFixture]
public class CalculatorTests
{
}
Teraz będziemy w niej dodawać metody testujące. Każdą swoją metodę powinieneś przetestować na różne sposoby (jeśli to jest sensowne). A więc do każdej metody napiszesz pewnie kilka metod testujących. I tutaj dwie uwagi.
- Metoda testująca powinna testować tylko jedną, konkretną rzecz – tak samo jak metoda w klasie powinna robić tylko jedną, konkretną rzecz
- W programowaniu obowiązuje żelazna reguła DRY (Don’t Repeat Yourself). Czyli nie piszemy analogicznego kodu kilka razy. Jeśli chodzi o testowanie, to zasada DRY nie jest już taka żelazna. Pamiętaj, że im więcej logiki masz w testach, tym więcej błędów możesz w tych testach popełnić (w samym kodzie testującym). Zatem testy staramy się pisać jak najprościej, nawet jeśli to wymaga duplikowania kodu. Rzecz jasna, możesz tworzyć jakieś metody pomocnicze i nikt Ci tego nie zabroni. Sam też tak robię. Ale podejdź do tego ostrożnie i zdroworozsądkowo.
Metoda testująca
OK, napiszmy więc metodę, która sprawdzi, czy Add
w ogóle działa i się nie wywala. Jeśli chodzi o nazewnictwo metod testujących, jest tutaj też pewna reguła. Być może nie jest to żelazna zasada, ale na pewno sensowna i wspomagająca. Więc staraj się jej trzymać.
Nazwa metody testującej powinna składać się z 3 części:
- nazwa metody, którą testujemy
- warunki, które testujemy
- spodziewany efekt
Tak jak mówiłem, to nie jest jakaś żelazna zasada, ale pomaga. Więc staraj się stosować to lub podobne wzory. Zatem napiszmy teraz metodę testującą:
public void Add_ValidArguments_DoesNotThrow()
{
}
Co to oznacza:
- testujemy metodę
Add
- testujemy ją w normalnych warunkach – a więc przy użyciu poprawnych argumentów (ValidArguments)
- spodziewamy się, że metoda nie rzuci żadnym wyjątkiem (DoesNotThrow). Możemy też spodziewać się, że metoda zwróci poprawny wynik, czyli trzeci człon moglibyśmy nazwać np. „ReturnsValidValue„. Napiszmy teraz ciało tej metody:
public void Add_ValidArguments_DoesNotThrow()
{
Calculator calc = new Calculator();
int result = calc.Add(2, 2);
}
W tym momencie tworzymy obiekt Calculator
i wywołujemy metodę Add
. Czyli dokładnie to, co zrobimy gdzieś w programie, wykorzystując obiekt Calculator
.
Teraz upewnijmy się, że otrzymany wynik jest taki, jakiego się spodziewamy:
public void Add_ValidArguments_DoesNotThrow()
{
Calculator calc = new Calculator();
int result = calc.Add(2, 2);
Assert.AreEqual(4, result);
}
Sprawdzenie poprawności testu
Jak widzisz jest tutaj metoda AreEqual
z klasy Assert
. Klasa Assert pochodzi z frameworka nUnit. Inne frameworki testujące też mogą mieć klasę o nazwie Assert z innymi metodami albo zupełnie inne sposoby na sprawdzenie wyniku. Jak widzisz, klasa Assert
ma sporo możliwości różnych sprawdzeń. Większość z nich jest „samoopisująca się”, ale opis ich wszystkich znajdziesz w dokumentacji nUnit.
Metoda AreEqual
sprawdza, czy zmienna result
ma spodziewaną wartość. W tym przypadku 4. Jeśli nie miałaby takiej wartości, test zakończyłby się niepowodzeniem.
To teraz zróbmy drugie sprawdzenie, upewnijmy się, że metoda Add nie rzuca wyjątku:
public void Add_ValidArguments_DoesNotThrow()
{
Calculator calc = new Calculator();
int result = 0;
Assert.DoesNotThrow(() => result = calc.Add(2, 2));
Assert.AreEqual(4, result);
}
Teraz, jeśli metoda Add
rzuci wyjątek lub rezultat będzie inny niż spodziewany, test zakończy się niepowodzeniem.
Jest jeszcze jedna rzecz, którą trzeba zrobić, żeby test rozpocząć. Jak pisałem wcześniej – możesz mieć w klasie testującej różne metody pomocnicze. A więc nie każda metoda musi być metodą testującą. Aby oznaczyć metodę jako testującą posłuż się atrybutem Test:
[Test]
public void Add_ValidArguments_DoesNotThrow()
{
Calculator calc = new Calculator();
int result = 0;
Assert.DoesNotThrow(() => result = calc.Add(2, 2));
Assert.AreEqual(4, result);
}
Uruchomienie testu
Super! Teraz otwórz sobie okienko TestExplorer (menu View -> Test Explorer). To jest okienko, w którym zobaczysz wyniki testów. To okno jest z VisualStudio, a rozszerzenie nUnit 3 Test Adapter, które zainstalowałeś wcześniej, pozwala na połączenie nUnit z mechanizmem testowania dostępnym w Visual Studio.
Ok, teraz zbuduj swoją solucję. Jeśli się nie buduje, prawdopodobnie otrzymałeś błąd, że .NetFramework 4.6 nie jest kompatybilny z .NetStandard. Po prostu kliknij prawym klawiszem myszy na projekt testujący i wybierz Properties. Tam przejdź na zakładkę Application i z comboboxa TargetFramework ustaw właściwość .Net Framework 4.7.2. Teraz powinieneś już móc zbudować wszystko.
Zobacz, jak wygląda okienko Test Explorer. Powinieneś tu zobaczyć wszystkie testy, jakie masz w solucji. Możesz uruchomić poszczególne testy, możesz uruchomić całą grupę. Jeśli klikniesz prawym klawiszem myszy na jakiś element (grupę testów albo poszczególny test) zobaczysz elementy Run i Debug. To Cię w tym momencie interesuje najbardziej. Polecenie RUN po prostu uruchomi testy. Polecenie DEBUG uruchomi testy w trybie debugowania. Tzn. że dopiero przy DEBUG będą istotne breakpointy, które postawisz w kodzie testującym.
Uruchom teraz test i zobacz wynik. Test możesz uruchomić na trzy sposoby:
- Klikając zielony PLAY na górze okienka TestExplorer -> w ten sposób możesz uruchomić od razu WSZYSTKIE testy
- Klikając prawym klawiszem myszy na konkretnym teście (w okienku TestExplorer) i wybierając opcję RUN.
- Klikając prawym klawiszem myszy na konkretnym teście (w okienku TestExplorer) i wybierając opcję DEBUG – w ten sposób będziesz mógł debugować swoje testy. Tylko przy tej opcji breakpointy w kodzie testowym będą aktywne.
Super! Wszystko się udało. To teraz w ramach ćwiczeń zrób coś, żeby test nie powiódł się i zobacz, jak to wygląda.
Testy jednostkowe dla wielu przypadków
Ok, wróćmy do poprawnych kodów i zróbmy jakiś lepszy test. W tym momencie testujemy tylko jeden przypadek – 2 + 2. Lepszy test przetestuje kilka przypadków. nUnit tutaj ułatwia sprawę. Wystarczy, że atrybut Test
zamienisz na TestCase
i zmienisz lekko metodę:
[TestCase(2, 2, 4)]
[TestCase(3, 3, 6)]
[TestCase(0, 2, 2)]
[TestCase(100, 150, 250)]
public void Add_ValidArguments_DoesNotThrow(int x, int y, int expected)
{
Calculator calc = new Calculator();
int result = 0;
Assert.DoesNotThrow(() => result = calc.Add(x, y));
Assert.AreEqual(expected, result);
}
Atrybut TestCase
po prostu przyjmuje pewne zmienne. Zmienne te są później przekazywane do metody, dlatego musisz mieć tutaj tyle parametrów, ile masz w atrybucie. Myślę, że całość sama się tłumaczy.
W taki sposób pisząc tylko jeden test, stworzyłeś 4 różne przypadki testowe (co zobaczysz w okienku Test Explorer).
Wypadałoby też dodać testy, które sprawdzą jak metoda zachowuje się z ujemnymi argumentami i co ważniejsze – z argumentami BRZEGOWYMI: int.MinValue, int.MaxValue. Tutaj wszystko zależy od konkretnego programu. No bo co się stanie, jeśli do maksymalnej wartości dodasz 1? Licznik się przekręci i otrzymasz minimalną wartość… Lub program się wywali… W zależności, co chcesz osiągnąć. To tylko przykład.
Co testować?
Mógłbyś teraz się pokusić o to, żeby testować każdą swoją metodę. Oczywiście nie ma to sensu w praktyce. Jeśli 100% Twojego kodu jest obłożonych testami, to coś jest nie tak. Co powinieneś testować? Kody, w których jest jakaś logika. Ale tylko swoje! To jest ważne, bo sporo osób łapie się na tym, że testują kody innych frameworków. To jest błąd. Z założenia, frameworki są już przetestowane. To oznacza, że jeśli klasę Calculator
miałbyś z innego frameworka, to wtedy nie powinieneś jej testować. Jeśli masz taki przypadek:
public IList<string> GetWords(string str)
{
return str.Split(' ');
}
To nie testuj takiej metody. Inaczej wyszłoby na to, że testujesz metodę Split
z klasy string
. To nie ma sensu.
Ale już coś takiego:
public IList<string> GetWords(string str)
{
var arr = return str.Split(' ');
return arr.ToList();
}
MOŻE mieć sens (ale nie musi). Masz tutaj trochę dodatkowej logiki. A co, jeśli przekażesz pustego stringa? Zmienna arr
będzie nullem. Więc metoda się wywali. I w tym przypadku testujesz już swój kod, a nie kod frameworka. Zwracaj na to uwagę.
To tyle jeśli chodzi o podstawy testów jednostkowych. Więcej w drugiej części artykułu, do której szalenie Cię zachęcam 🙂
Jeśli masz pytania lub znalazłeś błąd w tekście, napisz w komentarzu.