Files
ext.jeremy.guillot@maxicoffee.domains b52b27189d docs(claude): mise à jour skill testing-strategy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:17:12 +01:00

8.3 KiB

name, description, allowed-tools
name description allowed-tools
testing-strategy 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. 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

// 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

// 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

// 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<string, Manga> */
    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).

// 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

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.