fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
5 minut

[UT-3] Mocki


22.08.2011

[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…

0 0 votes
Article Rating
14 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
codebush
codebush
12 years ago

Zawiły post. Nie rozumiem go wcale.

procent
12 years ago

codebush,
Pytaj, w miarę możliwości rozjaśnię w komentarzach.

Maks
12 years ago

Moje doświadczenie z TypeMock’iem to trauma, problemy z Visual Studio po zainstalowaniu plugin’u bo TypeMock używa jakiegoś API do debuggowania. Oczywiście nie można zapomnieć o dodatkowej gimastyce przy integorowaniu TypeMock’a z serwerem build’ów, bo to jest full-wypas produkt a nie jedna DLL’ka.

Long live the Rhino Mocks ;)

zjaadc
12 years ago

Czy mógłbyś wyjaśnić co dokładnie dzieje się w linijce z wywołaniem metod Setup i Returns? Ta linijka jest kluczowa dla sensu istnienia całego mock’a.

procent
12 years ago

zjaadc,
Linijka ta konfiguruje przechwycenie sterowania przez Moq. Dzięki tej instrukcji wywołanie metody GetEuroRate na obiekcie _currencyRateSource zwróci wartość przekazaną do metody Returns(), czyli w tym przypadku – euroRate (4.35m). Taka konfiguracja jest możliwa, ponieważ pod spodem Moq generuje swoją własną implementację interfejsu ICurrencyRateSource (w momencie "new Mock<ICurrencyRateSource>()") i może dowolnie reagować na wywołania metod.

zjaadc
12 years ago

OK. A co w takim razie zwraca Setup(), skoro na tym czymś zwróconym jest wywoływane jeszcze Returns()? Rozumiem, że para wywołań metod Setup i Returns odpowiada skonfigurowaniu jednej "zaślepionej" metody.

procent
12 years ago

zjaadc,
Tak jest. Metoda Setup() pochodzi z Moq – jest metodą klasy Mock<T> – i nie jest częścią testowanego interfejsu. Zwraca specjalny interfejs Moq zawierający instrukcje pozwalające skonfigurować wartości zwracane, rzucane wyjątki, wykonanie jakichś pobocznych czynności itd. Więcej szczegółów tutaj: http://code.google.com/p/moq/wiki/QuickStart .
Wspomniane FakeItEasy podchodzi do tego trochę inaczej i może być prostsze do zrozumienia.

M.
M.
12 years ago

Po pierwsz gratuluje fajnie rozwija się ta seria post-ów
Po drugie to co do Typemock Isolator to wydaje mi się, że interesującą alternatywą, o porównywalnych możliwościach, jest darmowy projekt <a href="http://research.microsoft.com/en-us/projects/moles/&quot; rel="nofollow">http://research.microsoft.com/en-us/projects/moles/</a&gt;.
Np. Mockowanie DateTime.Now
Typemock Isolator

Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));

Moles

MDateTime.NowGet = () => new DateTime(2000, 1, 1);
procent
12 years ago

M.,
Dzięki, jakoś nigdy nie przyglądałem się Moles, faktycznie warto zacząć od tego przed kupnem Typemocka. Chociaż i tak moja rekomendacja pozostaje niezmienna – programować tak, aby dało się przetestować bez uciekania się do takich produktów:).

drucik
12 years ago

Witam,

Kolejną ciekawą alternatywą dla TypeMock jest Telerik JustMock (http://www.telerik.com/products/mocking.aspx projekt komercyjny z ograniczoną funkcjonalnie darmową wersją), który po za funkcjami mockowania metod statycznych/sealed/niewirtualnych umożliwia, także mockowanie zapytań LINQ i pojedynczych metod w stworzonym obiekcie. Dodatkowo umożliwia mockowanie dowolnych metod dowolnych klas, tak samo jak Moles wspomnianych przez M., np.:

Mock.Arrange(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));
Łukasz K.
12 years ago

O ile dobrze zrozumiałem całość postu to mocki mają za zadanie ukryć jeszcze nie zaimplementowaną część aplikacji lub za symulować pobranie danych z zewnętrznego źródła, czyż tak?

procent
12 years ago

drucik,
O, nawet nie wiedziałem że telerik ma cos takiego:). Jak widzę typemock ma sporą konkurencję.

procent
12 years ago

Łukasz,
Tak, dokładnie. Z tym, że niekoniecznie musi chodzić o pobranie danych – równie dobrze może to być wysyłanie:). Na przykład ostatnio pisałem komponent komunikujący się z jakimś webservicem i w testach sprawdzałem, czy mock interfejsu "WebRequestSender" otrzymywał z mojego systemu poprawnie skomponowane żądanie – niezależnie od tego co takie żądanie miałoby zwrócić.

Wojtek Turowicz
12 years ago

@Łukasz: chodzi o to żeby zasymulować stan niezależnego komponentu, na który nie mamy wpływu, aby przetestować poprawne działanie swojego komponentu.

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również