---
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<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).

```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.
