Czytając różne książki lub artykuły można się zgubić w terminologii. Szczególnie Mock oraz Stub często są używane zamiennie a ich różnica jest jednak znacząca. Nie tłumaczyłem na język polski powyższych terminów aby nie wprowadzać jeszcze większego zamieszania
Zacznijmy od terminu najbardziej ogólnego – Double. Nazwa określa dowolny obiekt naśladujący realną klasę. Zatem obiektem double może być Fake, Dummy, Stub lub Mock. Innymi słowy double można podzielić na wspomniane 4 obiekty. Termin stanowi po prostu ogólne określenie dowolnego obiektu, używanego podczas testów jednostkowych w celu izolacji. Nazwa wzięła się z filmów w których często pracę aktora w niebezpiecznych scenach zastępują dublerzy (kaskaderzy, stunt double).
Pierwszym typem Double jest Dummy, który stanowi najbardziej prymitywny sposób izolacji. Dummy używamy gdy nie ma dla nas znaczenia co dana klasa zrobi a po prostu zaprojektowana architektura wymagania przekazania jakiegoś obiektu. Wtedy tworzymy Dummy (atrapę), która po prostu nic nie robi. Dzięki temu będziemy mogli przetestować pozostałą funkcjonalność, izolując całkowicie klasę, której nie chcemy weryfikować. Załóżmy, że mamy klasę Order:
interface IMessageBoxService { Result Show(string message); } class Order { private IMessageBoxService _messageBoxService; private IStockRepository _stockRepository; public Product(IMessageBoxService msgService, IStockRepository stockRepository) { _messageBoxService = msgService; _stockRepository = stockRepository; } public bool Submit() { msgService.Show("Proszę czekać - sprawdzanie zamówienia"); int remainingQuantity=_stockRepository.GetQuantity(IdProduct); if( Quantity > remainingQuantity) return false; // ... - jakas inna logika msgService.Show("Gotowe!"); return true; } // pozostala czesc logiki }
W skrócie, za pomocą DI wstrzykujemy implementacje MessageBoxService oraz pewnego repozytorium służącego m.in. do sprawdzenia liczby sztuk towaru w magazynie. Następnie metoda Submit wyświetla dwa komunikaty oraz sprawdza czy żądana ilość towaru może zostać wydana. Oczywiście zadanie postawione to przetestowanie metody Submit. Załóżmy, że nie interesuje nas kompletnie to co robi MessageBoxService ponieważ jest to “tylko” warstwa prezentacji. Możemy użyć w tej sytuacji obiektu Dummy:
class DummyMsgBoxService: IMessageBoxService { public Result Show(string message) { return Result.OK; } }
Powyższa implementacja jest całkowitą atrapą – metoda nic tak naprawdę nie robi.
Kolejną, bardziej zaawansowaną konstrukcją jest obiekt Fake. W przeciwieństwie do Dummy, zawiera już pewną logikę jednak nie taką samą jak prawdziwy obiekt. W przypadku repozytorium, może używać zwykłą kolekcję danych (in-memory), zamiast odniesień do prawdziwej bazy danych.
Pozostały najbardziej mylone obiekty Stub i Mock. Stub używamy w testach jednostkowych wykorzystujących weryfikację stanu obiektów. Mock z kolei używamy w testach opartych na badaniu zachowania (behaviour-based testing). Zacznijmy od krótkiego opisu testów opartych na stanie obiektów ponieważ są one najbardziej popularne. Stwórzmy więc Stub dla repozytorium:
class StubStockRepository: IStockRepository { public int GetQuantity(int idProduct) { return 5; } }
Jak widać, Stub zawsze zwraca z góry zdefiniowane wyniki. Następnie test dla Submit będzie wyglądać następująco:
public void SubmitTest() { Order order=new Order(new StubStockRepository()){Quantity=10}; bool result = order.Submit(); Assert.IsFalse(result); }
Test sprowadzana się zawsze do weryfikacji stanu obiektu lub zwróconej wartości. W testach z wykorzystaniem Stub wywołujemy metodę a następnie sprawdzamy właściwości klasy aby upewnić się, że wywołanie metody zmieniło odpowiednie pola w klasie. Z kolei sam Stub zwraca predefiniowaną przez nas, przewidywalną wartość.
Mock opiera się na zachowaniach. Najpierw definiujemy nasze oczekiwania w formie jaka metoda powinna być wykonana, następnie wywołujemy testowaną metodę i sprawdzamy czy nasze oczekiwania zostały spełnione. Oczekiwania w przypadku metody Submit są następujące (przy założeniu, że w magazynie jest żądania ilość towaru):
-
Metoda Show (IMessageBoxService) powinna zostać wywołana dwa razy.
-
Metoda GetQuantity powinna zostać wywołana dokładnie raz. Ponadto w założeniu można zdefiniować przekazany parametr do GetQuantity – musi to być identyfikator produktu.
Z wykorzystaniem framework’u Moq, można test wykonać następująco:
[Test] public void SubmitTest() { var msgServiceMock = new Moq.Mock<IMessageBoxService>(); var stockRepositoryMock = new Moq.Mock<IStockRepository>(); Order order = new Order(msgServiceMock,stockRepositoryMock){Quantity = 3}; order.Submit(); // weryfikacja zdefiniowanych zachowań msgServiceMock.Verify(r => r.GetQuantity(Moq.It.IsAny()),Moq.Times.Once()); stockRepositoryMock.Verify(m => m.Show(Moq.It.IsAny()),Moq.Times.Exactly(2)); }
Zamiast sprawdzania wyniku zwróconego przez Submit, weryfikujemy które metody zostały wykonane. Możemy oczywiście uszczególnić i określić dokładnie jakie parametry powinny być przekazane, które metody nie powinny zostać wywołane oraz jakie operacje są dozwolone na właściwościach.
Więcej o testach typu behaviour, mam nadzieję, że napiszę wkrótce…