[ten post jest częścią mojego minicyklu o testach, pełna lista postów: tutaj]
Programiści .NET nie mogą narzekać na brak narzędzi i bibliotek wspomagających pisanie testów jednostkowych. Zanim przejdziemy jednak do zerkania w ich kierunku, zobaczymy jak można samemu, bez zewnętrznych zależności, rozpocząć pisanie testów.
Testować będziemy taką banalną klaskę, której zadaniem jest obliczenie "ile złotych polskich dostanie polski hydraulik Waldek za przywiezione zza zachodniej granicy jełro":
1: public class LocalEuroCalculator
2: {
3: public decimal Calculate(decimal srcEur, decimal euroRate)
4: {
5: return srcEur * euroRate;
6: }
7: }
Console application
Tak jest, nie pomyliłem się. Pierwszy przykładowy test jednostkowy zaimplementujemy w zwykłej aplikacji konsolowej, bez referencji do żadnych "testujących" bibliotek. Bo tak naprawdę co jest potrzebne do tego, aby mieć działający test jednostkowy? Po pierwsze - metodę testującą. I po drugie - coś, co tą metodę odpali i wyświetli rezultat jej działania.
No to proszę bardzo, oto metoda testująca, czyli nasz pierwszy test jednostkowy:
1: public class EuroCalculatorTests
2: {
3: public void Calculates_Pln()
4: {
5: decimal input = 50.0m;
6: decimal euroRate = 4.35m;
7: decimal correctResult = 217.5m;
8:
9: var calculator = new LocalEuroCalculator();
10: var result = calculator.Calculate(input, euroRate);
11:
12: if (result != correctResult)
13: {
14: throw new Exception(string.Format("Expected {0} but actual result was {1}", correctResult, result));
15: }
16: }
17: }
Jest git? Jest git.
(czytelników zastanawiających się nad tym, dlaczego w teście mam już wyliczoną wartość correctResult, a nie wyliczam jej na bieżąco, odsyłam do swojego posta sprzed półtora roku: "Jak nie pisać testów jednostkowych")
No to teraz ten test trzeba uruchomić:
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: var tests = new EuroCalculatorTests();
6:
7: tests.Calculates_Pln();
8:
9: Console.WriteLine("All tests passed");
10: }
11: }
I już. Gdy test przechodzi, widzimy coś takiego:

A gdy nie - coś takiego:

Ale dobra, koniec zabawy, przecież nikt nie pisze testów w ten sposób.
MSTest
Jako pierwszą z bibliotek do testów wymieniam MSTest z jednego powodu... aby móc od razu napisać: odradzam. Jakieś pliki .testsettings, jakieś konfiguracje, jakieś wizardy... po to żeby uruchomić test? Pisanie testów powinno być proste, szybkie i przyjemne. Kiedyś dałem szansę temu rozwiązaniu i naprawdę chciałem z niego skorzystać w prawdziwym projekcie. Ale..
Zresztą, żeby nie kombinować, przekleję swoje wrażenia z posta "Programowanie przez eksplorację":
"Przyznam, że do tej pory zawsze omijałem MS Test szerokim łukiem. Korzystałem albo z nUnit, albo z mbUnit. I w sumie nie wiedziałem dlaczego... przecież wbudowany w VS runner dla MSTest to naprawdę wielka zaleta, jeśli ktoś nie ma Resharpera. A czym takim niby może różnić się jedna biblioteka do testów jednostkowych od innej biblioteki do testów jednostkowych?
Ano... okazało się że może. Ludu mój ludu, nigdy więcej. Bardziej się tego chyba skomplikować nie da. Tu nie wystarczy dorzucić jednego i drugiego atrybutu żeby stworzyć test. Dodatkowo trzeba wypełnić jakąś chorą konfigurację, poczytać instrukcję... a ja chcę tylko odpalić test! Po TRZECH GODZINACH walki z atrybutem DeploymentItemAttribute, ustawianiem przeróżnych konfiguracji, męczenia się z tak banalnym zadaniem jak wykorzystanie zewnętrznego pliku w teście... powiedziałem sobie DOŚĆ. WIEM że to musi jakoś działać, WIEM że z pewnością wiele osób z tego korzysta, WIEM że dokumentacja na pewno jest poprawna... ale mimo to mi, zwykłemu kmiotowi, nie udało się zmusić tego szatańskiego pomiotu do wykonania odpowiednich zadań. Trzęsąc się z irytacji wróciłem do nUnit i po 5 minutach miałem wszystko działające tak jak chciałem, bez żadnego zbędnego wnikania w durne obejścia. Ale co się klawiaturze oberwało ciosów zadanych w furii, to się oberwało."
Heh, self-quotation:).
Jedna uwaga - podkreślę jeszcze raz, że wielką, niekwestionowaną zaletą MSTest jest zintegrowany z Visual Studio runner. Pewnie gdybym nie miał Resharpera to patrzyłbym na to trochę inaczej... chociaż z drugiej strony, kiedyś bez niego żyłem, uruchamiałem testy nUnitowe nawet z VS Express i też wcale nie było źle.
nUnit, mbUnit
Skoro to co trzeba mieć za sobą mamy już za sobą, pora na właściwe narzędzia. Najpopularniejszą biblioteką do testów w światku .NET jest nUnit. Test z jej wykorzystaniem wygląda tak:
1: [TestFixture]
2: public class CalcTest_NUnit
3: {
4: private LocalEuroCalculator _calc;
5:
6: [SetUp]
7: public void CreateCalculator()
8: {
9: _calc = new LocalEuroCalculator();
10: }
11:
12: [Test]
13: public void Calculates_Pln()
14: {
15: decimal input = 50.0m;
16: decimal euroRate = 4.35m;
17: decimal correctResult = 217.5m;
18:
19: decimal result = _calc.Calculate(input, euroRate);
20:
21: Assert.AreEqual(correctResult, result);
22: }
23: }
Dla mbUnit, alternatywnego rozwiązania, konstrukcja będzie identyczna.
Czyli jak widać - klasę z testami trzeba udekorować jednym atrybutem, metodę przygotowującą środowisko do testów - kolejnym atrybutem. Samą metodę testu - jeszcze innym atrybutem... Nie ma w tym tak naprawdę nic złego. Więc czy to koniec przedstawienia narzędzi?
Nie, bo...
xUnit
Sam od kilku miesięcy, gdy tylko mam wybór, sięgam po xUnit. Powód jest bardzo prosty: jeśli można uniknąć pisania nadmiarowego kodu to zawsze skorzystam z okazji. Popatrzmy:
1: public class CalcTest_XUnit
2: {
3: private LocalEuroCalculator _calc;
4:
5: public CalcTest_XUnit()
6: {
7: _calc = new LocalEuroCalculator();
8: }
9:
10: [Fact]
11: public void Calculates_Pln()
12: {
13: decimal input = 50.0m;
14: decimal euroRate = 4.35m;
15: decimal correctResult = 217.5m;
16:
17: decimal result = _calc.Calculate(input, euroRate);
18:
19: Assert.Equal(correctResult, result);
20: }
21: }
Żadnego [TestFixture] - skoro klasa zawiera testy to znaczy że trzeba je uruchomić. Żadnego [SetUp] - w końcu od tego mamy konstruktory. Po prostu - oznaczamy test atrybutem [Fact] i tyle. Sweet. Bardzo polecam (nieprzekonanych zapraszam do lektury uzasadnienia powstania xUnit).
A najfajniejsze jest to, że możemy sobie dowolnie te biblioteki łączyć i mieszać, a kochany Resharper i tak będzie potrafił nam je uruchomić (po dodaniu odpowiednich wtyczek):

Hint: pod tym adresem można znaleźć projekt wykorzystywany przeze mnie podczas prezentacji o testach. Stamtąd pochodzi kod zawarty w tym poście.