[ten post jest częścią mojego minicyklu o testach, pełna lista postów: tutaj]
Kiedyś puściłem cały cykl postów o testowaniu z wykorzystaniem Rhino Mocks, pełna lista postów zebrana jest tutaj.
Jednak aby zachować ciągłość aktualnej serii, należy o mockach kilka słów wspomnieć.
Mocki służą do symulowania zachowania środowiska zewnętrznego względem testowanej klasy/metody. Pisząc testy jednostkowe sprawdzające logikę biznesową nie powinniśmy skupiać się na tym, czy mamy poprawnie skonfigurowaną bazę danych. Albo czy komunikacja z systemem plików przebiega tak jak powinna. Lub, co jest klasycznym scenariuszem, na który nie mamy wpływu, czy wykorzystywany “obcy” serwis aktualnie działa czy nie.
Takie rzeczy powinny być pochowane za interfejsami (chociaż do bazy danych w tym kontekście jeszcze kiedyś pewnie wrócę). Interfejsy definiują z jakich funkcjonalności może skorzystać nasza klasa – a nie interesuje nas, w jaki sposób zostanie to zrealizowane.
Przykład
Niech poniższy przykład zobrazuje o co chodzi:
1: public interface ICurrencyRateSource 2: { 3: decimal GetEuroRate(); 4: } 5: 6: public class RemoteEuroCalculator 7: { 8: private readonly ICurrencyRateSource _currencyRateSource; 9: 10: public RemoteEuroCalculator(ICurrencyRateSource currencyRateSource) 11: { 12: _currencyRateSource = currencyRateSource; 13: } 14: 15: public decimal Calculate(decimal srcEur) 16: { 17: decimal euroRate = _currencyRateSource.GetEuroRate(); 18: 19: return srcEur * euroRate; 20: } 21: }
Tutaj również obliczamy “ile jewro dostaniemy za złocisze”, jednak kalkulator nie wymaga podania aktualnego kursu. Zamiast tego przyjmuje zależność (ICurrentRateSource), która potrafi tą informację SKĄDŚ zdobyć. Na etapie pisania kalkulatora nie interesuje nas skąd. Co więcej, możemy przetestować poprawne działanie kalkulatora bez posiadania implementacji tego interfejsu! W pierwszym poście serii pisałem o tym, jak testowanie wpływa pozytywnie na nasz kod poprzez zachęcanie do stosowania dobrych praktyk programistycznych, takich jak IoC. No i właśnie powyżej mamy tego przykład.
Test taki, przy użyciu biblioteki Moq (i przedstawionego poprzednio xUnit), wyglądałby następująco:
1: public class CalcTest_moq 2: { 3: private RemoteEuroCalculator _calc; 4: private Mock<ICurrencyRateSource> _currencyRateSource; 5: 6: public CalcTest_moq() 7: { 8: _currencyRateSource = new Mock<ICurrencyRateSource>(); 9: 10: _calc = new RemoteEuroCalculator(_currencyRateSource.Object); 11: } 12: 13: [Fact] 14: public void Calculates_Pln() 15: { 16: decimal input = 50.0m; 17: decimal euroRate = 4.35m; 18: 19: _currencyRateSource.Setup(x => x.GetEuroRate()) 20: .Returns(euroRate); 21: 22: decimal correctResult = 217.5m; 23: 24: decimal result = _calc.Calculate(input); 25: 26: Assert.Equal(correctResult, result); 27: } 28: }
Najpierw tworzymy mock i przekazujemy jego instancję jako zależność w konstruktorze kalkulatora. Potem, w teście, konfigurujemy zachowanie tej zależności. I… to tyle! Mając działający i przetestowany kalkulator możemy zająć się implementacją funkcjonalności “pobieranie aktualnego kursu euro”. Może to być usługa na stronach NBP, może to być jakaś baza, może wreszcie dostarczany z zewnątrz plik tekstowy.. .na tym etapie nie ma to żadnego znaczenia.
Należy jednak zdawać sobie sprawę z tego, że pisanie testów do obiektów zajmujących się głównie przekazywaniem sterowania pomiędzy zależnościami, bez żadnej właściwie logiki, może być męczące, kłopotliwe i… zbędne. Sprowadzać się może tylko do konfiguracji masy mocków, a następnie sprawdzania wywołania metod ze zwracanymi kolejno wartościami. Ale o tym w jednym z kolejnych postów.
Narzędzia
Podobnie jak w przypadku samych testów, tak i tutaj mamy całkiem sporo różnych bibliotek do wyboru. Kiedyś jedynym sensownym rozwiązaniem było wspomniane Rhino Mocks. Kilka lat temu był to chyba jedyny projekt, który podczas konfiguracji mocka nie wymagał przekazywania nazw metod w postaci stringów (sic!). Po wypuszczeniu .NET 3.5 realizacja tego zadania stała się dużo prostsza, dzięki takim mechanizmom jak lambda expressions i (bardziej) expression trees. Tym samym powstały kolejne projekty pozwalające na bardzo wygodne i bezpieczne (sprawdzane podczas kompilacji) zabawy z mockami. Ich API jest czystsze niż w Rhino, dźwigającym bagaż wieloletniej ewolucji. Dlatego też, pomimo sentymentu, od dość dawna nie decyduję się już na włączanie Rhino do swoich projektów. Zamiast tego stosuję zademonstrowane wyżej Moq. Ostatnio miałem też okazję pobawić się FakeItEasy i wydaje mi się (na podstawie kilkunastu linijek napisanego kodu oraz dokumentacji i przykładów), że to TA JEDYNA biblioteka. Właśnie od niej polecałbym zacząć zagłębiając się w świat mocków.
Kilka słów należy się też płatnemu, ale ciekawemu projektowi Typemock Isolator. Tym co odróżnia go od konkurencji jest możliwość zamockowania dosłownie WSZYSTKIEGO. Nie trzeba się bawić w IoC/DI, można mockować metody statyczne, metody sealed, metody niewirtualne… no dosłownie wszystko. Brzmi fajnie? Może i tak, ale… należy zdawać sobie sprawę z tego, że takie testy spełniają tylko jedną z ról testów jednostkowych: mogą sprawdzić poprawne działanie napisanego kodu. I tyle. Zalecałbym zatem raczej unikanie podobnych rozwiązań, chociaż czasem możemy nie mieć wyjścia. Jak inaczej przetestować na przykład rozwiązanie pisane na Sharepointa? Chyba się nie da…