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:
Serwisy z metodami void'owskimi te偶 weryfikujemy w inny spos贸b - sprawdzaj膮c ich z'mock'owane zale偶no艣ci, dlatego kolejno odpadaj膮 nam:
To co w艂a艣ciwie pozosta艂o to:
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:


