Testy jednostkowe z PHPUnit oraz Mockery

Jedną z głównych zasad dobrych testów jednostkowych jest izolacja. Jak w takim razie poradzić sobie z różnymi zależnościami? Z pomocą przychodzi nam Mockery – narzędzie do imitacji obiektów.

Głównym celem izolacji jest testowanie tylko jednej klasy na raz (a w zasadzie funkcjonalności). Stąd też pochodzi nazwa samych testów: testy jednostkowe. Bardzo często zdarza się jednak, że testowana klasa czy metoda korzysta z metod innych klas. Jednym z rozwiązań tego problemu (moim ulubionym) jest zastosowanie frameworka który imituje zachowanie wskazanych obiektów: Mockery.

Zakładam, że znasz PHPUnit, umiesz go zainstalować (najlepiej przez Composera) oraz wiesz jak z niego skorzystać i napisać podstawowy test. Jeżeli nie, to zapraszam na mojego bloga, zamierzam wkrótce popełnić serię artykułów na temat TDD gdzie na pewno przerobimy temat PHPUnit od podstaw (taka publiczna deklaracja zmotywuje mnie do pisania).

Geneza

Na potrzeby tekstu załóżmy, że posiadamy klasę (przykład będzie mega uproszczony, ale w praktyce wygląda to podobnie) która odpowiada za obsługę jakiegoś eventu naszej aplikacji. W czasie jego obsługi wysyła ona wiadomość e-mail. Postaram się posłużyć praktycznym przykładem: rejestracja klienta i wysłanie maila aktywacyjnego. Rozpatrzmy poniższą klasę:

Do przeprowadzenia testu potrzebna będzie również klasa Mailera (a raczej jej kiepska imitacja :), tak tego nigdy nie róbcie):

Napiszmy teraz klasę testu która przetestuje całość:

Uruchamiamy phpunit i okazuje się, że wszystko działa jak należy:

Wstępnie wydawałoby się, że możemy przejść dalej. Testy są zielone, wszystko działa. Czy oby na pewno? Okazuje się, że za każdym razem gdy uruchamiamy test wysyłany jest e-mail. Wyobraźmy sobie sytuację w której mamy 100 takich testów. Czy mamy czas na wysyłania 100 e-maili? Sam proces testowania na pewno też będzie trwał. Jeżeli to cię nie przekonuje, to zastanów się co tak naprawdę chcemy tutaj przetestować. W końcu klasa Mailer’a powinna posiadać swoje niezależne testy jednostkowe.

Rozwiązanie

Całkowita imitacja obiektu

Zanim przejdziemy do instalacji nowego narzędzia informuję, że możliwe jest przeprowadzenie podobnej „imitacji” obiektu przy pomocy samego PHPUnit. Na samym końcu przedstawię odpowiedni kod który robi to samo bez potrzeby instalacji Mockery. Sami ocenicie które z narzędzi jest bardziej czytelne (dosłownie).

Najpierw musimy w jakiś sposób „zaciągnąć” Mockery. Jeżeli korzystasz z Composera nie ma z tym najmniejszych problemów, jeden wpis załatwia sprawę:

Teraz pozostaje wpisać w konsoli composer install i możemy przejść do napisania tzw. „mocka” czyli obiektu który będzie imitował naszą klasę. Dodatkowo na zakończenie każdego testu musimy wywołać Mockery::close, który sprawdzi czy wszystkie makiety zostały użyte zgodnie z założeniami. Efekt ten uzyskamy dodając metodę tearDown() do klasy naszego testu. Kompletna nowa klasa testu będzie wyglądać następująco:

Najpierw tworzymy nową atrapę – w metodze mock podajmy nazwę klasy, którą ma imitować. Następnie na stworzonym obiekcie tworzymy szereg instrukcji, który przedstawia sposób imitowania obiektu. Stosowane metody utworzonej atrapy wykorzystują tzw. „fluent interface”, przez co możemy wpisywać je kolejno po sobie. Więc w powyższym przykładzie: na naszej atrapie ($mock) powinna zostać wywołana metoda sendActivationMessage (uzyskujemy to wywołując shouldReceive('sendActivationMessage')), jeden raz (once()) i powinna zwrócić logiczne true (andReturn(true)). Przyznacie sami, że składania jest samoopisująca się (o ile znamy język angielski).

Ok. Teraz uruchamiamy ponownie nasz test:

Jak widać różnica jest znacząca. Test pokrywa to co ma pokrywać oraz żaden e-mail nie jest wysyłany. W pokazany sposób możemy tworzyć atrapy dowolnych klas.

Częściowa imitacja obiektu

W celu zaprezentowania częściowej imitacji obiektu, musimy trochę „urozmaicić” nasz przykład. Załóżmy, że nasz event potrzebuje jeszcze klasy użytkownika aby pobrać jego e-mail. Dodatkowo dodamy metody do klasy Mailer (set i get):

Klasa Mailer po modyfikacjach:

Teraz możemy napisać test, który imituje wyłączanie metody sendActivationMessage, reszta metod będzie korzystać z pierwotnego obiektu:

W kwadratowym nawiasie wpisana została metodą którą będziemy emulować. Można wpisać tam więcej funkcji oddzielając je przecinkiem. Każda wybrana w ten sposób metodą powinna być obsłużona przez shouldReceive. Na końcu dodałem dodatkowe sprawdzenie (asercję), aby upewnić się, że działanie pozostałych metod (setEmail i getEmail) jest prawidłowe. W ten sposób przekonamy się, czy faktycznie emulowana jest tylko wybrana przez nas metoda. Nie pozostaje nam nic innego jak uruchomić test:

Jak widać na potrzeby tego testu klasa User nie musi nawet istnieć. Kod testu i opisanych klas dostępny pod adresem: https://github.com/itcraftsmanpl/PHPUnitAndMockeryTest

Poniżej przedstawiam dodatkowe metody jakie oferuje Mockery (są one na tyle dobrze nazwane, że nie trzeba ich nawet komentować):

Na zakończenie prezentuję jeszcze w jaki sposób można imitować obiekt natywnie przy użyciu PHPUnit:

Jak widać robi się to bardzo podobnie, ale otrzymany kod jest mniej czytelny. Na dzisiaj to wszystko. Zachęcam do komentowania i udostępniania – może się przydać :).

Entuzjasta programowania. Z zawodu web developer. Pragmatyk. Od jakiegoś czasu również przedsiębiorca. Racjonalista. W wolnych chwilach biega i bloguje. Miłośnik gier i grywalizacji.

  • sbl

    Ciekawy wpis, czekam na dalsze części i bardziej skomplikowane przykłady + wstęp dla newbie do PHPUnit/Behat.

  • „zamierzam wkrótce popełnić serię artykułów na temat TDD gdzie na pewno przerobimy temat PHPUnit od podstaw (taka publiczna deklaracja zmotywuje mnie do pisania).” – trzymam za słowo.

  • ghost1511

    Czy autor może podać zalety korzystania z Mockery zamiast korzystania ze standardowego ‚mockowania’ w PHPUnit? Przydałby się również bardziej zaawansowany przykład, obiektu który nie zwraca typu prostego, a obiekt.

    • Osobiście wydaje mi się, że składnia i zapis Mockery jest dużo bardziej czytelna od mockowania natywnego w PHPUnit, a co za tym idzie Mockery będzie prostszy w użyciu. Do tego Mockery można używać wszędzie, nie tylko w PHPUnit ale również PHPSpec.

      Nie twierdzę, że Mockery jest lepszy, ale zawsze jest to ciekawa alternatywa. Poza tym, kto się nie zgodzi, że super się to czyta: $user->shouldReceive(‚method’)->once()->andReturn(‚value’);

      Można by tu wspomnieć jeszcze o Hamcrest, który poprawił by czytelność jeszcze bardziej, ale na razie nie chciałem komplikować.

      Jeżeli chodzi o zwracanie, to do metody andReturn() można wstawić cokolwiek, nawet nowego mocka (choć lepiej wcześniej go „wskrzesić” i nadać mu jakieś metody wymagane): $mock->shouldReceive(‚method’)->once()->andReturn(Mockery::mock(‚Mailer’));

    • Kuba Turek

      Korzystałem zarówno z PHPUnit’s mocks i z Mockery i uważam, że obie biblioetki są równie dobre, ale z tego co wiem, tylko PHPUnit umożliwia póki co mockowanie kontruktorów klas – może się przydać jeśli DI działa właśnie przez konstruktor.

  • Rafał Łużyński

    testy gdzie potrzebne są mocki, to przeważnie testy akceptacyjne, a tych powinno być mało. Jednostkowo powinno się testować logike biznesową i wtedy obchodzimy się bez mocków i stubów. W przeciwnym razie szybko będziemy mieli mase testów, które bardzo ciężko utrzymać i w efekcie trzeba je usuwać. Polecam prezentację Sławomira Sobótki na temat testowania: https://www.youtube.com/watch?v=znRByMgnFSM

    • Test akceptacyjny to nic innego jak sprawdzenie czy kod spełnia oczekiwania klienta. Nie widzę tutaj związku.

      Z kolei jeżeli chcemy zachować zasadę „Single responsibility principle” to nadajemy każdej klasie jedną odpowiedzialność. W takim przypadku trzeba jakoś rozwiązać zależności (DI, IoC, itp.) i trzeba mieć możliwość ich przetestowania. Przy czym, ja osobiście wystrzegam się większej liczby zależności jak 4. Jeżeli klasa potrzebuje kolejnych 5 do działania to staram się to podzielić.

      • Filip Górny

        Nic dodać nic ująć. Kolega chyba korzysta z active record.

    • Potfur

      W zależności od szkoły/osobistych preferencji: wszystkie DOCy są mockowane, część z nich albo żaden – nie to jednak określa rodzaj testu.

      W testach jednostkowych testujesz zachowanie konkretnego elementu (np. klasy) w maksymalnie dużej ilości przypadków. Czy funkcje/metody dla określonych danych wejściowych zwracają oczekiwane wartości, czy odpowiednio reaguje na błędne dane (wyrzuca wyjątki) itd.

      W innych testach sprawdzasz inne rzeczy.

    • Rafał Łużyński

      Nie zrozumieliśmy się. Im bardziej zewnętrzna warstwa kodu (bliżej infrastruktury), tym więcej mamy przypadków do przetestowania, jeżeli testujemy jednostkowo. Im niżej (bliżej logiki biznesowej) tym tych przypadków jest mniej (bo są rozbite na mniejsze części). Dlatego najlepiej jednostkowo testować logikę biznesową, a akceptacyjnie wyższe warstwy, te które mają zależności (serwisy aplikacyjne, serwisy infrastuktury). Logika biznesowa nie wymaga zależności (chyba, że jest źle zamodelowana), stąd nie potrzeba w niej mocków. Jako, że najważniejszą częścią naszej aplikacji jest właśnie logika biznesowa, to ona powinna być porządnie przetestowana jednostkowo. Nie potrzebujemy 100%, ani nawet 60% coverage, testujemy core domain – to co jest dla nas najważniejsze w aplikacji.
      W artykule dodałbym jeszcze, że powinno się testować jedynie zachowania (settery i gettery to nie zachowania). Jeżeli zachowania są dobrze przetestowane, to gettery i settery siłą rzeczy też będą sprawdzone, jeżeli nie – są niepotrzebne.

      • Potfur

        Tak dla jasności – to co byś mockował? Poproszę o typowe mockowane przez ciebie obiekty.

        • Rafał Łużyński

          Właśnie o to chodzi. Mocki niepotrzebnie komplikują testy, przedłużają proces ich pisania i utrudniają utrzymanie, dlatego staram się ich unikać. Jak już trzeba, a przeważnie dzieje się to testując serwis aplikacyjny (orkiestrujący logikę biznesową), to mockuję repozytoria i inne serwisy infrastruktury. W powyższym artykule takim serwisem jest „mailer”. Nie mówię, że mocki są złe, bo się przydają, ale nie zaczynajmy nauki testowania kodu od mocków, bo to powinna być jedna z ostatnich rzeczy.

  • Bartek

    Jakby ktoś był zainteresowany pisaniem płynnych asercji w stylu:

    $animals = [‚cat’, ‚dog’, ‚pig’];
    Assert::thatArray($animals)->hasSize(3)->contains(‚cat’);

    Albo prostszym mockowaniem (nie expectations-based):

    Mock::when($mock)->someMethod(‚arg’)->thenReturn(‚result’);

    To zachęcam do spróbowania Ouzo Goodies: https://github.com/letsdrink/ouzo-goodies

  • Marcin

    Fajnie przetestowałeś 😀 pokaż jak testować z metody do których musisz wstrzyknąć jakiś parametr. Bo z tym jest większy problem.

Send this to friend

webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL
webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL