Natknąłem się ostatnio na taki artykuł Testing Without Mocks: A Pattern Language
Artykuł dość długi, ale cała idea jest przedstawiona w sekcji Example. Z grubsza chodzi o to, żeby implementując infrastrukturę, napisać też implementację, która nie zależy od zewnętrznych narzędzi typu baza danych czy jakieś API. Taki "konfigurowalny" stub, ale to kod produkcyjny. Autor nazywa to Nullable. Potem w testach używamy tego Nullable zamiast mockować bazę, 3rd party API czy system plików.
Klepnąłem sobie prostą apkę z testami w takim stylu i muszę powiedzieć, że jest to całkiem przyjemne, ale strasznie mnie razi w oczy kod "tylko do testów" poplątany z kodem produkcyjnym. Już nie mówię nawet o tych Nullable'ach, ale to podejście wymaga czasem dodatkowych wywołań klas/metod, które służą tylko temu, żeby łatwiej (albo w ogóle) można było napisać asercję lub złapać wynik/efekt uboczny wywołania jakiejś funkcji. Przykład prosto od autora artykułu: implementacja CommandLine ma listener, który jest tam tylko po to, żeby potem w teście móc przechwycić output.
Po drugiej stronie spektrum jest podejście opisane tutaj (wybaczcie za film, nie mogę nigdzie tego znaleźć w formie tekstu). Tutaj z kolei autor poleca następujące podejście:
- zdefiniować zależności modułu (w OOP to będzie interface lub protocol),
- mockować zależności i sprawdzić, czy są dobrze wywoływane, np. jeśli testuję handler HTTP, to testuję, czy na GET /search&q=foobar wywołany zostanie SearchService.getResults("foobar"),
- stubować zależności i sprawdzić, czy handler HTTP jest w stanie obsłużyć wszystkie możliwe zwrotki od swoich zależności, np. stubuję SearchService tak, by getResults zwracało: pustą tablicę, jeden element, kilka elementów, dużo elementów i rzuciło wyjątek,
- moduł uznaję za przetestowany, a testując SearchService trzymam się zasady, że muszę napisać testy, które:
- wywołują getResults z takimi parametrami, jakie sprawdzałem gdy mockowałem ten serwis w testach handlera HTTP, i osobno takie, które
- robią setup w taki sposób, by getResults zwróciło mi wyniki, które zastubowałem testując handler HTTP
Po takich testach mam mieć pewność, że http handler poprawnie rozmawia ze swoimi zależnościami i potrafi obsłużyć to, co te zależności zwrócą, oraz, że service przyjmie i obsłuży to, co handler mu przekaże i w odpowiedniej sytuacji zwróci to, czego handler oczekuje.
Plus jest taki, że nie mieszam kodu produkcyjnego z testowym i nie muszę pisać dodatkowego kodu produkcyjnego tylko po to, żeby móc coś (łatwiej) przetestować. Minus jest taki, że jest dużo mocków, a mówią, że mocki są złe :)
Próbowałem po trochu obu podejść i o ile w drugim jest więcej klepania, mam wrażenie, że te testy są czytelniejsze, bardziej zwięzłe i nie testują tego samego 10 razy. Do tego łatwiej mi pisać aplikację "top-down", mogę się skupić na jednym module na raz, zależności zastubować/zamockować i martwić się nimi później. Jednakże używanie tylu mocków i stubów po prostu "doesn't feel right". Do tego nie ma narzędzi do sprawdzenia, czy wszystkie przypadki zakładane w testach handlera HTTP są przetestowane w testach serwisu, więc powiązanie jest dość luźne i wydaje się, że łatwo można popełnić błąd lub o czymś zapomnieć. Czy jest jakiś middleground między jednym a drugim, albo czy preferujecie jedno podejście od drugiego? Co sądzicie o samej idei przeplatania kodu produkcyjnego z testowym? Co sądzicie o pisaniu kodu produkcyjnego, który jest używany tylko w testach?