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.
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-ATestovací 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:
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í.
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.
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.
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.
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ě.
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.
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.
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.