Files
Mangarr/.claude/skills/testing-strategy/SKILL.md
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

259 lines
8.3 KiB
Markdown

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