--- name: testing-strategy description: Stratégie de tests du projet Mangarr — pyramide adaptée à l'archi DDD/Hexa. Tests unitaires purs sur le Domain/Application (sans framework), adapters InMemory, tests fonctionnels API. Utiliser quand on crée ou modifie des tests, ou qu'on discute de la couverture à implémenter. allowed-tools: Read, Grep, Glob --- # Stratégie de tests — Mangarr ## Pyramide ``` ┌─────────────────────────────┐ │ Tests Fonctionnels (API) │ ← peu nombreux, coûteux │ tests/Functional/ │ zenstruck/browser + BrowserKit ├─────────────────────────────┤ │ Tests d'Intégration │ ← adapters Doctrine, clients HTTP │ tests/Domain/*/Adapter/ │ zenstruck/foundry + DAMA ├─────────────────────────────┤ │ Tests Unitaires (Domain) │ ← majorité, rapides, sans framework │ tests/Domain/*/Application/ │ PHPUnit pur, InMemory adapters └─────────────────────────────┘ ``` --- ## 1. Tests Unitaires — Application Layer (CommandHandlers, QueryHandlers) **Localisation :** `tests/Domain/{Domain}/Application/CommandHandler/` et `QueryHandler/` **Principe :** Aucune dépendance au framework. On injecte des **adapters InMemory** à la place des vraies implémentations Infrastructure. ### Structure d'un test CommandHandler ```php // tests/Domain/Manga/Application/CommandHandler/CreateMangaHandlerTest.php namespace App\Tests\Domain\Manga\Application\CommandHandler; use App\Domain\Manga\Application\Command\CreateManga; use App\Domain\Manga\Application\CommandHandler\CreateMangaHandler; use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository; use App\Tests\Domain\Manga\Adapter\InMemoryImageProcessor; use App\Tests\Shared\Adapter\InMemoryMessageBus; use PHPUnit\Framework\TestCase; class CreateMangaHandlerTest extends TestCase { private InMemoryMangaRepository $repository; private CreateMangaHandler $handler; protected function setUp(): void { $this->repository = new InMemoryMangaRepository(); $this->handler = new CreateMangaHandler( $this->repository, new InMemoryImageProcessor(), new InMemoryMessageBus(), ); } public function testHandleSuccess(): void { // Arrange $command = new CreateManga( title: 'One Piece', slug: 'one-piece', // ... ); // Act $this->handler->handle($command); // Assert $saved = $this->repository->findAll()[0]; $this->assertEquals('One Piece', $saved->getTitle()->getValue()); } public function testThrowsWhenInvalid(): void { $this->expectException(\RuntimeException::class); $this->handler->handle(new CreateManga(title: '', /* ... */)); } } ``` ### Structure d'un test QueryHandler ```php // tests/Domain/Manga/Application/QueryHandler/GetMangaByIdHandlerTest.php class GetMangaByIdHandlerTest extends TestCase { private InMemoryMangaRepository $repository; private GetMangaByIdHandler $handler; protected function setUp(): void { $this->repository = new InMemoryMangaRepository(); $this->handler = new GetMangaByIdHandler($this->repository); } public function testThrowsWhenNotFound(): void { $this->expectException(MangaNotFoundException::class); $this->handler->handle(new GetMangaById('non-existent')); } public function testReturnsMappedResponse(): void { // Arrange — construire l'Aggregate directement avec Value Objects $manga = new Manga( id: new MangaId('123'), title: new MangaTitle('One Piece'), // ... ); $this->repository->save($manga); // Act $response = $this->handler->handle(new GetMangaById('123')); // Assert — vérifier les scalaires du Response $this->assertEquals('123', $response->id); $this->assertEquals('One Piece', $response->title); } protected function tearDown(): void { $this->repository->clear(); } } ``` --- ## 2. Adapters InMemory **Localisation :** `tests/Domain/{Domain}/Adapter/` Chaque interface de `Domain/Contract/` a son adapter InMemory dans les tests. Ces adapters stockent les données en mémoire (`array`). ### Structure d'un InMemory Repository ```php // tests/Domain/Manga/Adapter/InMemoryMangaRepository.php namespace App\Tests\Domain\Manga\Adapter; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Model\Manga; class InMemoryMangaRepository implements MangaRepositoryInterface { /** @var array */ private array $mangas = []; public function save(Manga $manga): void { $this->mangas[$manga->getId()->getValue()] = $manga; } public function findById(string $id): ?Manga { return $this->mangas[$id] ?? null; } public function findAll(): array { return array_values($this->mangas); } public function clear(): void { $this->mangas = []; } // ... implémenter toutes les méthodes de l'interface } ``` ### Adapters InMemory disponibles (existants) | Adapter | Interface implémentée | |----------------------------------|------------------------------------------| | `InMemoryMangaRepository` | `MangaRepositoryInterface` | | `InMemoryImageProcessor` | `ImageProcessorInterface` | | `InMemoryMangadexClient` | `MangadexClientInterface` | | `InMemoryMangaProvider` | `MangaProviderInterface` | | `InMemoryPathManager` | `MangaPathManagerInterface` | | `InMemoryMessageBus` | `MessageBusInterface` | Quand on crée une nouvelle interface dans `Domain/Contract/`, **créer l'adapter InMemory correspondant** avant d'écrire les tests. --- ## 3. Tests Fonctionnels API **Localisation :** `tests/Functional/` Utilisent `zenstruck/browser` + `BrowserKitBrowser` avec le conteneur Symfony complet. Les données sont gérées par `zenstruck/foundry` (Factories) et `DAMA\DoctrineTestBundle` (rollback automatique après chaque test). ```php // tests/Functional/SomeEndpointTest.php namespace App\Tests\Functional; use Zenstruck\Browser\Test\HasBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class SomeEndpointTest extends WebTestCase { use HasBrowser; public function testGetManga(): void { $this->browser() ->get('/api/mangas/by-id/some-uuid') ->assertStatus(200) ->assertJson() ->assertJsonMatches('title', 'One Piece'); } public function testCreateManga(): void { $this->browser() ->post('/api/mangas', [ 'json' => ['externalId' => 'abc-123'], ]) ->assertStatus(204); } } ``` --- ## Commandes ```bash make test # tous les tests make test f="CreateMangaHandlerTest" # un test par nom de classe make test c="--group unit" # par groupe make test c="--stop-on-failure" # s'arrêter au premier échec ``` --- ## Checklist par feature Quand on implémente une nouvelle feature, les tests à écrire dans l'ordre : 1. **Test du CommandHandler/QueryHandler** (unitaire, `TestCase` pur) - Cas nominal (happy path) - Cas d'erreur (not found, invalide…) - Vérification que le repository est bien appelé 2. **Test de la Value Object** si une nouvelle VO est créée - Validation des invariants (cas invalides) - `getValue()` retourne la bonne valeur 3. **Test fonctionnel de l'endpoint** (si API Platform) - Codes HTTP corrects (200, 201, 204, 404…) - Structure JSON de la réponse Ne pas tester les Processors/Providers API Platform en unitaire (trop de couplage framework) — les couvrir via les tests fonctionnels.