259 lines
8.3 KiB
Markdown
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.
|