Kapitola 17 · Praxe · Testování DDD kódu v Symfony

Testování DDD kódu v Symfony

Testování Domain-Driven Design kódu v Symfony v praxi. Unit testy doménové vrstvy, integrační testy s Doctrine, funkční testy API, InMemory repozitáře, testování doménových událostí a architektonické testy s Deptrac.

Autor M. Katuščák
Doba čtení ≈ 30 min
Náročnost pokročilá
Publikováno · Aktualizováno ·
Obsah kapitoly

17.01 Filozofie testování v DDD#

Doménová vrstva v DDD nezávisí na frameworku ani na databázi, takže ji lze testovat přímo z PHPUnitu bez bootstrappingu Symfony kernelu. To je hlavní praktický rozdíl proti tradičním vrstveným architekturám, kde jsou unit testy svázané s kontejnerem a kde běh tisíce testů trvá minuty. V DDD běží stejný počet testů v sekundách. Stavební kameny doménové vrstvy – entity, hodnotové objekty, agregáty, doménové události – popisuje kapitola Základní koncepty DDD.

FIG. 17.1-A Testovací pyramida pro DDD aplikaci – poměr a obsah jednotlivých vrstev

Testovací pyramida pro DDD

Testovací pyramida (koncept popularizovaný Mikem Cohnem v knize Succeeding with Agile, 2009 [1]) rozděluje testovací sadu do tří vrstev. Liší se rychlostí, mírou izolace a tím, kolik kódu jeden test pokryje:

17.02 Unit testy doménové vrstvy#

Unit testy doménové vrstvy tvoří základ testovací sady DDD aplikace. Pokrývají největší podíl kódu, běží v řádu milisekund a nepotřebují nic jiného než PHPUnit a samotné doménové třídy. Žádný bootstrap Symfony kernelu, žádná databáze, žádné fixtures.

Testování Value Objects

Test value objektu ověřuje tři věci: že neplatný vstup vyhodí odpovídající výjimku, že dvě instance se stejnou hodnotou jsou si rovny přes equals(), a že objekt zůstává neměnný – jiná hodnota znamená novou instanci. Tím je hodnotový objekt pokrytý.

Testování entit

Test entity ověřuje, co entita dělá, ne jak vypadají její fieldy. Volá se veřejná metoda, ověřuje se výsledný stav přes další veřejné metody a u zakázaných operací se očekává konkrétní doménová výjimka. Přístup k privátním vlastnostem přes reflexi je signál, že test sleduje implementaci místo chování.

Testování agregátů

Agregát chrání konzistenci skupiny entit a vydává doménové události. Test agregátu má proto dvě role: ověřit transakční invarianty (pravidla platná pro celý agregát po každé operaci) a zkontrolovat, že operace vydala očekávané události ve správném pořadí.

17.03 Testování doménových událostí#

Doménové události jsou způsob, jak agregát mluví se zbytkem systému. Test proto ověřuje přímo to, co agregát po operaci vydá – typ události, její data a pořadí během jedné transakce. Spoléhat se na vedlejší efekt event dispatcheru je křehké a do unit testu přibírá zbytečnou závislost. Pokud váš systém používá události jako zdroj pravdy, doplňující strategie testování auditovatelnosti a rebuildu projekcí najdete v kapitole Event Sourcing.

17.04 Test doubles a InMemory repozitáře#

Test double je obecný název pro náhradu reálné závislosti v testu. Meszarosova taxonomie z xUnit Test Patterns rozlišuje pět typů: dummy, stub, spy, mock a fake. V praxi s PHPUnit se pracuje hlavně se čtyřmi z nich (stub, mock, fake, spy) a v DDD má každý jiný dopad: vede k jinému stylu testu a k jiné odolnosti vůči refaktoringu.

InMemory repozitář zde unikátnost e-mailu jen aproximuje. Finální záruku dává unikátní constraint v databázi – kanonický handler překládá jeho porušení na DuplicateEmailException (viz race condition v naivní variantě). Test ověřuje chování handleru, ne řešení souběhu.

17.05 Integrační testy s Doctrine#

Integrační testy odpovídají na otázku, kterou unit testy pokrýt nemohou: zda Doctrine mapování, dotazy repozitářů a transakce skutečně dělají to, co jejich rozhraní slibuje. Spouští se proti reálné databázi – typicky SQLite in-memory pro rychlost, nebo testovací instanci PostgreSQL/MySQL pro shodu s produkcí.

Obě implementace přitom plní tutéž smlouvu rozhraní: findById(UserId $id): ?User vrací při nenalezení null, stejně jako InMemory varianta ze sekce o test doubles. Druhou běžnou konvencí je metoda getById(), která místo null vyhazuje UserNotFoundException – projekt si vybere jednu variantu a drží ji ve všech implementacích i testech.

17.06 Funkční testy API a kontrolerů#

Funkční test prochází celý zásobník: request přijde do kontroleru, projde aplikační vrstvou, dotkne se databáze a vrátí odpověď. Ověřuje se HTTP status kód, tělo (typicky JSON), hlavičky a chování při chybových vstupech. V DDD je to jediná vrstva testů, která ověří, že prezentace s aplikační vrstvou spolu skutečně mluví správně.

17.07 Testování asynchronních toků#

Asynchronní zpracování přes Messenger rozděluje tok na dvě poloviny: odeslání zprávy a její zpracování workerem. Test musí pokrýt obě, každou zvlášť. Architekturu busů popisuje kapitola CQRS, transakční předávání zpráv kapitola Outbox Pattern.

In-memory transport

V testovacím prostředí nahradí reálný broker transport in-memory://. Zprávy nikam neodcházejí, zůstávají v paměti procesu a test si je vyzvedne přímo z kontejneru. Stačí přepsat DSN pro prostředí test:

Funkční test pak ověří, že endpoint zprávu skutečně odeslal – aniž by čekal na workera:

zenstruck/messenger-test

Holé assertions nad transportem fungují, ale v každém testu se opakují. Balíček zenstruck/messenger-test je zabalí do čitelného API: trait InteractsWithMessenger zpřístupní frontu transportu a metoda process() zpracuje zařazené zprávy přímo v testu, bez spouštění workera.

Test idempotence handleru

Asynchronní transport doručuje zprávy v režimu at-least-once: po pádu workera dorazí tatáž zpráva podruhé. Handler proto musí být idempotentní – dvojí zpracování smí vyvolat jen jeden efekt. Deduplikační mechanismus popisuje Idempotent Inbox – záznam se ukládá pod dvojicí eventId (ULID) a consumer. Test je krátký: zavolat handler dvakrát se stejnou zprávou a spočítat efekty.

Test outboxu

Outbox dává dvě garance a každá potřebuje vlastní integrační test. První: po flush() leží událost v outbox tabulce, zapsaná ve stejné transakci jako agregát. Druhá: relay ji publikuje do transportu a označí jako zpracovanou. Obě varianty relay procesu rozebírá kapitola o Outbox Pattern.

Druhý test záměrně končí dvojicí assertions: zpráva odešla a fronta pending záznamů je prázdná. Pokud by relay publikoval, ale neoznačil záznam jako zpracovaný, příští běh by událost poslal znovu.

17.08 Architektonické testy#

Pravidlo, že doménová vrstva nesmí záviset na infrastruktuře ani na aplikační vrstvě, drží jen do první uspěchané code review, ve které někdo přidá use Doctrine\ORM\Mapping do entity. Architektonické testy tomu zabraňují technicky: pravidla závislostí jsou popsána deklarativně a porušení padne v CI jako spadlý test, ne až v review.

Deptrac

Deptrac je nástroj od QOSSMIC (dříve sensiolabs-de) pro statickou analýzu závislostí v PHP projektech. Definujete vrstvy (layers) a povolená pravidla závislostí (ruleset). Deptrac analyzuje závislosti v kódu a nahlásí porušení. Spouští se v CI jako součást statické analýzy.

phparkitect jako alternativa

Pravidla závislostí umí vynutit i phparkitect (phparkitect/phparkitect). Na rozdíl od Deptrac s YAML konfigurací používá PHP API: pravidla se zapisují do souboru phparkitect.php jako typovaný kód s podporou IDE. Nástroj má vlastní CLI a nespouští se přes PHPUnit – v CI běží jako samostatný krok vedle testovací sady. Instalaci a přehled pravidel uvádí kapitola Méně známé vzory; plnou konfiguraci pro modular monolith ukazuje kapitola DDD a microservices. Zde stačí zapojení do pipeline:

Volba mezi oběma nástroji je věcí preferencí týmu. Deptrac popisuje vrstvy deklarativně v YAML a hodí se pro plošná pravidla mezi vrstvami; phparkitect dovolí jemnější pravidla nad jednotlivými třídami (suffixy názvů, konkrétní namespace). Oba selžou v CI stejně – jako spadlý build.

17.09 Code coverage a doporučené postupy#

Code coverage měří, jaké procento řádků kódu se při běhu testů provede. Sama metrika nic neříká o kvalitě testů – 100% pokrytí lze dosáhnout testy, které jen volají metody bez assertů. Užitečná je ale opačně: tam, kde je pokrytí nízké, leží kód, který nikdo netestuje. Tam stojí za to se podívat.

Testovací pyramida v DDD funguje díky tomu, že doménová vrstva je čistý PHP bez závislostí na frameworku. Tisíce unit testů proto běží v sekundách. Integrační a funkční testy doplňují pokrytí tam, kde vstupuje infrastruktura, a architektonické testy hlídají, aby tato izolace nezmizela při dalším refaktoringu.

Časté otázky

Jak testovat agregát – unit test s mock repozitářem, nebo integrační test?

Agregát se testuje primárně unit testem – je to čistý PHP bez závislostí na frameworku nebo databázi. Test instancuje agregát, volá jeho metody a ověřuje výsledný stav i vyvolané doménové události. Mock repozitáře přitom není potřeba, protože samotný agregát repozitář nevolá. Integrační test doplňuje pokrytí až na úrovni, kde vstupuje persistence – tedy při ukládání a načítání agregátu. Podrobný rozbor v sekci Unit testy doménové vrstvy.

K čemu slouží InMemory repozitář a kdy ho preferovat před mockem?

InMemory repozitář je plnohodnotná implementace rozhraní repozitáře, která drží agregáty v poli v paměti. Oproti mocku simuluje reálné chování (najít, uložit, počítat), takže testy aplikačních služeb procházejí celý use case věrohodněji. Mock se hodí tam, kde je potřeba ověřit konkrétní interakci – kolikrát byla metoda volána a s jakými argumenty. InMemory repozitář naopak slouží pro ověření výsledku, ne volání. Rozbor variant v sekci Test doubles a InMemory repozitáře.

Jak ověřit, že agregát publikuje správné doménové události?

Po vykonání metody se z agregátu vyčte seznam zaznamenaných událostí (typicky přes releaseEvents()) a testem se ověří jejich typ, pořadí i obsah. Kontroluje se, že agregát vyvolal přesně ty události, které má, a nevyvolal žádné navíc. Pro funkční test lze stejné události zachytávat přes Messenger event bus a ověřit reakce dalších částí systému. Praktický příklad v sekci Testování doménových událostí.

Mají se testovat privátní invarianty agregátu, nebo jen veřejné rozhraní?

Testuje se pouze veřejné rozhraní – chování agregátu přes metody, které se reálně volají z aplikační vrstvy. Privátní invarianty jsou detailem implementace a jejich přímé testování sváže test s konkrétní strukturou kódu, což brání refaktoringu. Dobře navržený test ověřuje, že po sérii veřejných volání je agregát ve validním stavu, vyvolal očekávané události a při porušení pravidla vyhodil konkrétní doménovou výjimku. Detailní rozbor v sekci Unit testy doménové vrstvy.

Co jsou architektonické testy a co kontrolují?

Architektonické testy automaticky ověřují, že kód dodržuje zvolená pravidla struktury – například že doménová vrstva nezávisí na Doctrine, že agregáty nevolají repozitáře přímo, nebo že kontrolery nekomunikují s infrastrukturou. V Symfony se používá nástroj Deptrac, který pravidla popisuje deklarativně v YAML a spouští se jako další testovací sada. Porušení pravidla se projeví jako spadlý test, nikoli až při code review. Rozbor nástrojů a pravidel v sekci Architektonické testy.