How to mock final class

Podobno ludzie dzielą się na dwie grupy tych, którzy robią backupy oraz na tych, którzy dopiero zaczną. Podobnie jest z nami programistami, dzielimy się na dwa obozy: jedni, którzy piszą testy oraz drudzy, którzy dopiero zaczną pisać testy jednostkowe.

Dzisiejszy wpis jest zaadresowany dla jednych i drugich. Gdy zaczynamy pisać testy lub też posługujemy się nimi na co dzień, przychodzi taki moment w życiu każdego z nas, w którym w pasku wyszukiwania ulubionej wyszukiwarki wpisujemy: how to mock final class.

Mockowanie

Mocki były, są i będą częścią testów jednostkowych. Używamy ich codziennie w niewyobrażalnych ilościach. Bywają proste, trywialne, powtarzalne, jedno-linijkowe, ale czasem trafią się takie, które zajmują nawet 70% testu.
Zależności między obiektami to normalna rzecz. Nierzadko są to zależności do obiektów nienależących do nas. Ale zasada jest prosta: najlepiej wszystkie obiekty tworzone poza obrębem testowanej klasy, lecz potrzebne do prawidłowego funkcjonowania danej instancji, powinny być mockami, ponieważ testujemy tylko odpowiedzialność danej klasy, przynajmniej jeśli mówimy o testach jednostkowych… a o takich traktuje ten wpis.

Sposoby na final’a

Gdy natrafimy już na final class, która np. jest wynikiem jakiejś zależnościach naszego komponentu, musimy sobie z tym jakoś poradzić. O tyle łatwo, jeśli dana klasa implementuje jakiś interfejs, wtedy sprawa jest prosta. Mockujemy po prostu interfejs, ale nie zawsze jest tak pięknie.
Klasa jest prosta, płytka i oznaczona jako final. Dla całkowitego uproszczenia, niech jej ciało wygląda tak:

Przykładowa klasa z zależnością wygląda następująco:

Jak widzimy na przykładzie, nasza klasa jest zależna od innej klasy, która produkuje finalną klasę. Odpowiedzialnością naszej klasy jest odpowiednie zmodyfikowanie / sformatowanie / wyświetlenie danych pochodzących z klasy finalnej. Wyobraźmy sobie, że chcemy teraz naszą klasę przetestować. Możliwości jest naprawdę niewiele. Przedstawię Ci trzy z nich.

Pierwsza z nich, nie tylko przyda Ci się do mockowania final’ów, ale także w prosty sposób może pomóc Ci zamockować obiekt, którego setupowanie jest bardzo uciążliwe. Sposób ten to nic innego jak klasa anonimowa. Prosty i przyjazny mechanizm, który pozwala nam utworzyć klasę na wzór tej, którą chcemy zamockować.

Uzupełniając powyższy przykład o klasę anonimową, test może wyglądać następująco:

Powyższy sposób pozwolił nam utworzyć zaślepkę dla obiektu, którego nie moglibyśmy zamockować ze względu na final.

Ktoś kiedyś powiedział mi, że dany test nic nie testuje i, że takie mocki są złe. Niestety nie mogę zgodzić się z takim wywodem. Ponieważ mock to mock, więc i tak musimy znać zachowanie mockowanego obiektu. Musimy dokładnie wiedzieć jakiej zwrotki oczekujemy i taką też będziemy tworzyć w trakcie mocka. Sposób utworzenia mocka, jest tutaj drugorzędny. I tak też, utworzenie obiektu jako klasa anonimowa i zasymulowanie jego zachowania zgodnie z oryginałem, uważam za całkowicie poprawny (jeżeli skłaniają się ku temu okoliczności, a za takie uważam np. final class).

Coś innego?

Drugim sposobem jest natomiast ingerencja w mechanizm ładowania pliku przez interpreter. W momencie czytania pliku z dysku jest on przetwarzany na odpowiednie tokeny. Sposób ten polega na usunięciu oznaczenia final w trakcie tokenizowania danej klasy. Na potrzeby testów jednostkowych powstały nawet do tego odpowiednie paczki np.: https://github.com/dg/bypass-finals

Ostatnim sposobem jest modyfikacja dostępności klasy już po jej zainicjalizowaniu. Do tego potrzebne jest odpowiednie rozszerzenie (które zainstalujemy z PECL), dlatego jest to najmniej elegancki sposób. Po więcej odsyłam tutaj: https://www.php.net/manual/en/book.uopz.php

Klasa anonimowa nie taka straszna

Jak widać, sposobów radzenia sobie z final classem mamy kilka. O dziwo łatwiej sobie poradzić z metodą oznaczoną jako final niż z całą klasą. Z powyższych sposobów ja pozostanę dalej przy klasie anonimowej, ale każdy ma prawo do swojego wyboru. A jakiego Ty dokonasz? Tylko nie tego najgorszego… brak testu!