Pokazywanie post贸w oznaczonych etykiet膮 Testy. Poka偶 wszystkie posty
Pokazywanie post贸w oznaczonych etykiet膮 Testy. Poka偶 wszystkie posty

sobota, 7 stycznia 2023

Weryfikacja stanu obiektu gdy stan nie jest ujawniony

 

Spotka艂em si臋 ostatnio z ciekawym przypadkiem weryfikowania wyniku testu jednostkowego. Testowaniu zosta艂a poddana void'owska metoda Agregatu i nijak nie mo偶na by艂o wyci膮gn膮膰 na zewn膮trz stanu obiektu, kt贸ry powinien ulec zmianie w wyniku wywo艂ania metody under test, by mo偶na by艂o go zweryfikowa膰 z oczekiwaniami. Agregat nie posiada艂 powi膮zanych z tym stanem getter'贸w, wygenerowane event'y nie mog艂y zosta膰 jawnie zwr贸cone poza obiektu (\Prooph\EventSourcing\AggregateRoot tego nie udost臋pnia艂) i 偶adna inna metoda jakkolwiek nie wyrzuca艂a stanu na zewn膮trz. Dodanie takich funkcji na si艂臋, tylko na potrzeby wykonania testu jednostkowego wydawa艂o si臋 nadu偶yciem wi臋c te偶 takie rozwi膮zanie nie wchodzi艂o w gr臋.

Przez powy偶sze problemy, trzeba by艂o wykaza膰 si臋 pomys艂owo艣ci膮 by napisa膰 test - kolega z zespo艂u wpad艂a na takie oto rozwi膮zanie:

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');

    // When
    // ...

    Closure::fromCallable(function () {
        TestCase::assertSame('Jan', $this->firstName);
        TestCase::assertSame('Kowalski', $this->lastName);
    })->bindTo($objectUnderTest, Person::class)();
}

Gdy pierwszy raz zobaczy艂em takie podej艣cie by艂em zaskoczony 偶e tak si臋 da, zaraz potem uzna艂em 偶e tak nie powinno si臋 robi膰. Tyle raz powtarzano, 偶e testowaniu powinno podlega膰 jedynie publiczne API obiektu, a jego wewn臋trzny stan powinien zosta膰 w ukryciu... i jest to m膮dre podej艣cie. Dzi臋ki temu mamy woln膮 r臋k臋 w przeprowadzaniu refaktoru - raz napisany test, daje nam mo偶liwo艣膰 weryfikowania czy ulepszanie wewn臋trznej struktury obiektu po zmianach dzia艂a dalej tak jak tego oczekujemy.   

To rozwi膮zanie 艂ami臋 zasad臋, wi膮偶膮c test z ukrytymi przed reszt膮 kodu produkcyjnego wn臋trzno艣ciami Agregatu. Potem jednak zacz膮艂em si臋 zastanawia膰 na ile realnie problematyczny b臋dzie on w utrzymaniu i czy czasem od dawna nie mierzymy si臋 z tego typu problemami.

馃敀 Kod produkcyjny dalej nie wie o stanie Agregatu

Niew膮tpliw膮 zalet膮 zastosowania metody Closure::bindTo jest fakt, 偶e nie wp艂ywa ona w 偶adnym stopniu na kod produkcyjny. Nie utworzyli艣my specjalnych getter'贸w wykorzystanych tylko na potrzeby test贸w co w przysz艂o艣ci mog艂oby sk艂oni膰 innych developer贸w do skorzystania z nich. W pewnym stopniu getter jest form膮 udost臋pniania warto艣ci 1:1 z tym co jest wewn臋trznym stanem obiektu. Dodanie getter'a wygl膮da jak rozszerzenie publicznego API, ale tylko na poz贸r. Bo je偶eli zmienimy wewn臋trzny stan to b臋dziemy musieli zmieni膰 owego getter'a. Mo偶na by zastanowi膰 si臋 nad innymi realnymi problemami z tym zwi膮zanymi, ale jest to temat na inny wpis.   

W ka偶dym razie wydaje mi si臋, 偶e problem tworzenia takich getter'贸w istnieje w 艣wiatku PHP, jest stosunkowo cz臋sto stosowany i tylko sprawia wra偶enie niegro藕nego - dlatego powinni艣my zwraca膰 na to uwag臋. 

Stosowanie Closure::bindTo zamias dedykowanych getter贸w chroni kod produkcyjny.

馃嚞馃嚙 Szko艂a Londy艅ska

Pisz膮c test jednostkowy klasy typu serwis (np. Event Subscriber'a, Command Handler'a) wed艂ug paradygmat贸w tz. Szko艂y Londy艅skiej musimy najpierw zamockowa膰 jego zale偶no艣ci. Mo偶na to zrobi膰 za pomoc膮 customowych implementacji InMemoryRepository b膮d藕 przy u偶yciu narz臋dzi takich jak Prophecy/MockObject.

I tutaj w艂a艣nie zacz膮艂em dostrzega膰 podobie艅stwo wzgl臋dem opisywanego przyk艂adu. Dok艂adnie wiemy jak w te艣cie jednostkowym nale偶y zbudowa膰 zale偶no艣膰 wymagan膮 przez object under test oraz jak b臋dzie wygl膮da艂a interakcja pomi臋dzy zale偶no艣ci膮, a testowanym obiektem.

Musimy jawnie oprogramowa膰 to zwi膮zanie by test m贸g艂 przej艣膰 na zielono. Test oczywi艣cie weryfikuje publiczne API serwisu, ale wie te偶 co艣 o jego wewn臋trznej pracy. Zmieniaj膮c zale偶no艣ci serwisu b臋dziemy musieli zaktualizowa膰 testy - dok艂adnie tak samo jak w opisywanym przypadku wykorzystania Closure::bindTo.

Jest jednak subtelna r贸偶nica, w przypadku Closure::bindTo funkcja anonimowa musi dok艂adnie wiedzie膰 w jakim polu znajduje si臋 warto艣膰 czyli po偶膮dana przez nas zmiana stanu. W przypadku weryfikowania mock'贸w za艣, ta informacja w dalszym ci膮gu jest przed nami ukryta, ale niew膮tpliwie w jednym i drugim przypadku odwo艂ujemy si臋 podczas wykonywania asercji do wewn臋trznego stanu obiektu.   

Jak wida膰 ten problem towarzyszy艂 w projekcie w kt贸rym pracuje praktycznie od samego pocz膮tku jego powstania, a mimo to da艂o si臋 z nim 偶y膰 - co wi臋cej - nikt nie uznawa艂 tego za problematyczne. W opisywanym przypadku pojawi艂 si臋 jedynie w nieco innej formie (inny typ testowanego obiektu), ale to dalej nic nowego z czym wcze艣niej si臋 nie borykali艣my.  

馃梼 Dla jakich typ贸w obiekt贸w?

Na samym pocz膮tku musz臋 zauwa偶y膰, 偶e chodzi o zmian臋 stanu obiektu dlatego z tego rozwiazania nale偶a艂oby korzysta膰 tylko w przypadku testowania metod void'owskich. Takich metod nie posiadaj膮 obiekty typu:

❌ Event
❌ DTO
❌ Value Object

Serwisy z metodami void'owskimi te偶 weryfikujemy w inny spos贸b - sprawdzaj膮c ich z'mock'owane zale偶no艣ci, dlatego kolejno odpadaj膮 nam:

❌ Event Subscriber
❌ Command Handler
❌ Serwis Aplikacyjny/Domenowy

To co w艂a艣ciwie pozosta艂o to:

✅ Encja
✅ Agregat 

Lecz tylko w przypadku gdy takowe ju偶 nie udost臋pniaj膮 swojego stanu dla innego kodu produkcyjnego!

馃┗ Inny spos贸b udost臋pniania stanu

Taki sam efekt mo偶naby osi膮gn膮膰 stosuj膮c refleksje:

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');

    // When
    // ...

    $reflection = new ReflectionClass(Person::class);
    $firstNameProperty = $reflection->getProperty('firstName');
    $firstNameProperty->setAccessible(true);
    $lastNameProperty = $reflection->getProperty('lastName');
    $lastNameProperty->setAccessible(true);

    self::assertSame('Jan', $firstNameProperty->getValue($objectUnderTest));
    self::assertSame('Kowalski', $lastNameProperty->getValue($objectUnderTest));
}

W por贸wnaniu z zastosowaniem Closure::bindTo wypada podobnie je偶eli chodzi o podej艣cie czy konsekwencje dla potencjalnego refaktoru. Wi臋cej linii kodu negatywnie wp艂ywa na czytelno艣膰 i w w膮tpliwo艣膰 mo偶emy poddawa膰 szybko艣膰 dzia艂ania, co nie powinno stanowi膰 problemu gdy ten kod tak czy inaczej nie b臋dzie dzia艂a艂 produkcyjnie. 

♻️ Warstwa abstrakcji na asercj臋

Mo偶na by dodatkowo schowa膰 tak膮 implementacj臋 przed testem w customowej klasie Assert. Je偶eli z biegiem czasu pojawi si臋 jaki艣 spos贸b na lepsze udost臋pnianie stanu ni偶 za pomoc膮 Closure::bindTo lub refleksji to sama klasa testowa nie b臋dzie wymaga艂a modyfikacji. Dodatkowo wprowadzenie specjalnej klasy asercji wp艂ynie na popraw臋 czytelno艣ci samego testu.

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');
    
    // When
    // ...
    
    PersonAssert::sameName('Jan', 'Kowalski', $objectUnderTest);
} 

馃摑 Podsumowanie

Koniec ko艅c贸w uwa偶am, 偶e zastosowanie Closure::bindTo by艂o dobrym pomys艂em. Pomimo tego, 偶e udost臋pniamy prywatny stan na zewn膮trz, napisany test jednostkowy przynosi warto艣膰. Co prawda jeste艣my mniej odporni na zmiany klasy poddanej testom, ale z drugiej strony nie mieli艣my zbytniego wyboru. Mogliby艣my u偶y膰 test贸w integracyjnych lecz nie s膮 one tak precyzyjne jak testy jednostkowe. Niekiedy nie dysponujemy takim rozwi膮zaniem w projekcie co uniemo偶liwi艂oby ca艂kowicie otestowanie takiej klasy. Korzystanie z Closure::bindTo powinno by膰 ograniczone tylko do absolutnej konieczno艣ci gdy nie mamy innego wyboru, jest ono obarczone pewnym obci膮偶eniem, ale na tyle ma艂ym by w dalszym ci膮gu utrzyma膰 wszystko w ryzach. 

Jedyne nad czym trzeba si臋 zastanowi膰 to czy weryfikacja dzia艂ania obiektu jest wi臋cej warta ni偶 poni偶sze efekty uboczne:

⚠️ zmiana wewn臋trznego stanu wymaga naprawienia testu
⚠️ zmiana nazwy pola sprawi 偶e komunikat nieprzechodz膮cego testu nie b臋dzie do ko艅ca jasny (UndefinedProperty)
⚠️ narz臋dzia do analizy statycznej mog膮 zg艂asza膰 b艂膮d w zwi膮zku z nieznanym polem

    

 

sobota, 15 pa藕dziernika 2022

Mockowanie Repozytori贸w

Testowanie jednostkowe klas posiadaj膮cych jako zale偶no艣ci Repozytoria np. Seriws贸w/Command Handler'贸w mo偶e by膰 k艂opotliwe. Je偶eli zdycydujemy si臋 na sztuczne implementacje Repozytori贸w tzw. *InMemoryRepository – powinni艣my by膰 艣wiadomi kwestii zwi膮zanych z ich p贸藕niejszym utrzymaniem – jakie problemy rozwi膮zuj膮, a jakie stwarzaj膮. Kod metody poddawanej testom nie jest tym nad czym chacia艂bym si臋 skupi膰 w tym wpisie, istotne jest jedynie wywo艂anie w niej metody Query (CQS) Repozytorium.

 

Podej艣cia do Mock’owania Repozytori贸w

Na potrzeby testu jednostkowego musimy odtworzy膰 wiern膮 kopi臋 klasy produkcyjnej. Np. posiadamy interfejs repozytorium OrderRepository:

interface OrderRepository 
{
	/** @return Order[] */
	public function getUnpaidOrders(): iterable;
}

 

Abstrahuj膮c od tego jak wygl膮da艂aby implementacja klasy produkcyjnej OrderMySQLRepository, skupmy si臋 na jej odpowiedniku utworzonym na potrzeby testu:

final class OrderInMemoryRepository implements OrderRepository
{
	/** @param Order[] $orders */
	public function __construct(private array $orders) {}
	
	public function getUnpaidOrders(): iterable
	{
		return array_filter(
			$this->orders,
			static fn ($order) => $order->isUnpaid() 
		);
	}
}


Implementacja metody OrderInMemoryRepository::getUnpaidOrders zosta艂a tak napisana by zawsze zwraca艂a odpowiedni膮 kolekcj臋 zam贸wie艅 - logicznie zgodn膮 z nazw膮 metody. Dzi臋ki takiej implementacji dysponujemy ca艂kiem por臋cznym narz臋dziem do pisania test贸w jednostkowych. W przypadku gdy interfejs posiada艂by by inne metody – te偶 w takim stopniu odtwarzaj膮ce rzeczywist膮 implementacj臋 – mogliby艣my u偶ywa膰 tego samego obiektu OrderInMemoryRepository w wielu przypadkach testowych. Jest to do艣膰 z艂o偶ony TestDouble typu Fake, kt贸ry niejako posiada wi臋dz臋 na temat tego jak dzia艂a produkcyjna implementacja.

Mogliby艣my przyj膮膰 inn膮 taktyk臋 w kt贸rej OrderInMemoryRepository jest maksymalnie okrojony z implementacji:

final class OrderInMemoryRepository implements OrderRepository
{
	/** @param Order[] $orders */
	public function __construct(private array $orders) {}
	
	public function getUnpaidOrders(): iterable
	{
		return $this->orders;
	}
}

 

Jako, 偶e OrderInMemoryRepository::getUnpaidOrders zawsze zwraca tak膮 sam膮 kolekcj臋 jak ta dostarczona do konstruktora – mo偶na powiedzie膰, 偶e jest to pewna forma Stub’a.Ci臋偶ar doboru odpowiednich danych wej艣ciowych spoczywa na metodzie w kt贸rej sztuczne Repozytorium zosta艂o utworzone – np. Fixtura, metoda testowa/setUp.

Korzystanie z wariantu Stub jest niemal identyczne jakby艣my u偶ywali biblioteki mokuj膮cej np. MockObject czy Prophecy. W dw贸ch przypadkach musimy staranie dobra膰 zwrotk臋 metody do aktualnego przypadku testowego. Pomi臋dzy przygotowan膮 kolekcj膮 obiekt贸w wrzucan膮 w metod臋 obiektu z biblioteki mokuj膮cej/czy konstruktorem Stub *InMemoryRepository, a zwr贸ceniem danych z wywo艂anej metody Repozytorium, nic wi臋cej si臋 nie dzieje - nie nast臋puje 偶adne filtrowanie.

Fake Repository

❌ jest silnie sprz臋偶ony z Produkcyjnym Repozytorium (po ka偶dej zmianie repozytorium MySQL, musimy sprawdzi膰 czy *InMemoryRepository nie wymaga modernizacji, czy w dalszym ci膮gu wiernie odzwierciedla prawdziw膮 implemetacj臋),

✅ koncept stoj膮cy za nazw膮 metod jest jawnie zapisany w kodzie,

✅ odpowiednio przygotowany mo偶e by膰 wykorzystany w wielu przypadkach testowych.

 

Stub Repository

❌ musi by膰 indywidualnie przygotowany pod ka偶dy przypadek testowy. Dob贸r zwrotek jego metod stoi po stronie developera – jest wi臋c nie do ko艅ca jawny,

❌ w przypadku zmiany metody Produkcyjnego Repozytorium, musimy zweryfikowa膰 wszystkie przypadki testowe kodu, kt贸ry bezpo艣rednio polega na tej metodzie. Czy w dalszym ci膮gu przygotowane przez developera obiekty zwracane przez metod臋 sztucznego Repozytorium spe艂niaj膮 logik臋 kt贸ra stoi za ich nazw膮? 

✅ zalet膮 jest mniej kodu do utrzymania, ale to tylko dlatego, 偶e koncepty te nie s膮 zawarte w kodzie z czego wynikaj膮 opisane wy偶ej problemy. W przypadku gdy istnieje tylko jeden klient metody Repozytorium mo偶emy zastosowa膰 Stub’a – wraz z rozrostem repozytorium o nowe metody, zwi臋kszeniem si臋 liczby klient贸w nale偶y rozwarzy膰 refaktoryzacj臋 w stron臋 Fake’a.

Koniec ko艅c贸w, czy stosujemy jedn膮 czy drug膮 metod臋 – ca艂y czas jeste艣my w tej samej sytuacji – musimy inscenizowa膰 dostarczanie danych do testowanej metody. W przypadku Fake’贸w z bardziej skomplikowan膮 implementacj膮, dysponujemy po prostu bardziej wszechstronnym narz臋dziem kosztem jego p贸藕niejszego utrzymania. 

 

Utrzymanie klasy Fake InMemory Repository


Nowe wymagania biznesowe w czasie kolejnych iteracji wymuszaj膮 zmian臋 repozytori贸w - prawdziwych i sztucznych.

Przedstawione na diagramie zmiany interfejsu Repozytorium, s膮 oczywi艣cie nap臋dzane decyzjami biznesu co do nowych feature’贸w. Jak wida膰 testuj膮c jednostkowo metod臋 wywo艂uj膮c膮 metody Repozytorium, musimy wprowadzi膰 do systemu testowego nowy byt. Ka偶da zmiana implementacji repozytori贸w b臋dzie wymusza艂a na nas prace zwi膮zane z jego utrzymaniem. Mo偶na wywnioskowa膰, 偶e klasy *InMemoryRepository to dodatkowy koszt jaki musimy ponie艣膰 za cen臋 bezpiecze艅stwa czyli 艂atwiejszego wprowadzania zmian w projekcie.

B艂臋dne b臋dzie jednak za艂o偶enie, 偶e Mock’owanie Repozytori贸w zawsze daje nam wspomniane bezpiecze艅stwo. W przypadku gdy pomi臋dzy Klas膮 Produkcyjn膮, a Test Double pojawi膮 si臋 rozbie偶no艣ci w dzia艂aniu – wprowadzone nie艣wiadomie/omy艂kowo przez programist臋 – testy jednostkowe mog膮 przechodzi膰 na zielono, podczas gdy tzw. Produkcja b臋dzie rzuca艂 b艂臋dami lub dzia艂a艂a niezgodnie z oczekiwaniami. Jest to ryzyko z kt贸rego powinni艣my zdawa膰 sobie spraw臋 podczas synchronizacji Fake InMemory Repository z Repozytorium Produkcyjnym.

Warto te偶 zauwa偶y膰, 偶e im bardziej uniwersalna jest metoda repozytorium tzn. posiada filtry, tym trudniej utworzy膰 jej imitacj臋 potrzebn膮 do test贸w jednostkowych. Odtworzenie poprawnego zachowania wszystkich filtr贸w mo偶e by膰 skomplikowane, a wprowadzona z艂o偶ono艣膰 z tym zwiazana - podatna na b艂臋dy w przysz艂o艣ci. Przy tego typu pracach nale偶y zachowa膰 szczeg贸ln膮 ostro偶no艣膰, gdy偶 jak na ironi臋, nie posiadamy testu jednostkowego klasy InMemory.

Je偶eli za艣 chodzi o sam design, na tak uniwersalnej metodzie Repozytorium polega膰 b臋dzie zapewne du偶o klient贸w, co b臋dzie skutkowa艂o pojawieniem si臋 zale偶no艣ci mi臋dzy nimi czego wsp贸lnym mianownikiem jest wspomniana „uniwersalna” metoda Repozytorium.

 

Podej艣cie funkcyjne

Mo偶emy przyj膮膰 zupe艂nie inn膮 taktyk臋. Zamiast zastanawia膰 si臋 nad najlepszym sposobem testowania metod kt贸re pozyskuj膮 dane z repozytori贸w - nie testowa膰 ich w og贸le. Wi膮偶e si臋 to z destylacj膮 logiki biznesowej zawartej w testowanej metodzie poprzez wydzielenie wszystkich wyow艂a艅 metod Query (CQS) do warstwy wy偶ej.

Metoda Serwisu samy pozyskuje dane z Repozytorium. Taka implementacja wymaga ich mock'owania.

 

W wyniku takiej zmiany design’u: wydestylowany byt staje si臋 Serwisem Domenowym, a warstwa kt贸ra pozyskuje Encj臋/Agregaty/VO z repozytori贸w – Serwisem Aplikacyjnym, kt贸rego domenowy odpowiednik otrzymuje wszystkie potrzebne do przeprowadzenia operacji obiekty, jako parametry metody.

 

Testowany Serwis Domenowy poddawany nie wie nic o klasach Repozytori贸w.

Testowaniu jednostkowem podlega膰 b臋dzie wtedy tylko Serwis Domenowy, dla kt贸rego nie b臋dziemy musieli ju偶 szykowa膰 偶adnego Test Double Repozytorium. Serwisy Aplikacyjne b臋d膮 testowane tylko Integracyjne.

Niekt贸re przypadki mog膮 okaza膰 si臋 problematyczne do zaimplementowania – bo jak mamy rozwi膮za膰 problem jakiego艣 bytu kt贸ry jest pobierany z repozytorium na podstawie jakiej艣 decyzji biznesowe?

Podsumowanie

Jak wida膰 mockowanie repozytori贸w wi膮偶e si臋 z pewnego rodzaju problemami z kt贸rych nale偶y sobie zdawa膰 spraw臋 – jak zwykl臋 w programowaniu nic nie jest czarno bia艂e i rozwi膮zuj膮c pewien problem godzimy si臋 na wprowadzenie mniejszego. Testy Integracyjne niejako rozwi膮zuj膮 spraw臋 tworzenia Test Double Repozytori贸w w og贸le, niemniej jednak nie zawsze takowe z 艂atwo艣ci膮 mo偶na wprowadzi膰 w projekcie.