Chapitre 14.4 — Fixtures, mocks & data providers
Partie 14 : Tests & qualité — Chapitre 4/7 Version de référence : PHPUnit 11 / Symfony 7.4.
⏱️ TL;DR
- Les fixtures de test préparent un état de base connu. Foundry (
zenstruck/foundry) offre des factories expressives :BookingFactory::createMany(10).- Un stub renvoie des valeurs prédéfinies (
createStub) ; un mock vérifie en plus les interactions (createMock+expects(...)).MockHttpClientsimule les API externes (chapitre 13.1) — tests rapides et déterministes, sans réseau.- Les data providers (
#[DataProvider]) exécutent le même test avec plusieurs jeux d’entrées → couverture large, peu de code.- Pour un dev Next.js : factories ≈ factory-bot/faker, stub/mock ≈
vi.fn()/vi.mock(), data providers ≈test.each.
🎯 Objectifs
- Préparer des données de test avec des factories.
- Distinguer stub et mock, et les utiliser à bon escient.
- Simuler des appels HTTP externes.
- Paramétrer un test avec un data provider.
1. Fixtures de test avec Foundry
Préparer des entités à la main dans chaque test est verbeux. Foundry définit des factories réutilisables :
composer require --dev zenstruck/foundry// src/Factory/BookingFactory.php (généré par make:factory)
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
final class BookingFactory extends PersistentProxyObjectFactory
{
public static function class(): string { return Booking::class; }
protected function defaults(): array
{
return [
'customerEmail' => self::faker()->email(), // Faker intégré
'start' => \DateTimeImmutable::createFromMutable(self::faker()->dateTimeBetween('now', '+30 days')),
'seats' => self::faker()->numberBetween(1, 4),
'status' => BookingStatus::Pending,
];
}
}Dans un test :
BookingFactory::createOne(['status' => BookingStatus::Confirmed]); // 1 réservation confirmée
BookingFactory::createMany(20); // 20 réservations aléatoires
$confirmed = BookingFactory::createMany(5, ['status' => BookingStatus::Confirmed]);Les factories rendent la préparation des données lisible et flexible (on surcharge juste les champs qui comptent pour le test). C’est plus maniable que des fixtures statiques (Partie 6.4) pour les tests.
2. Stub vs mock
Deux types de doubles, pour deux intentions :
- Stub : « renvoie ceci quand on appelle cela ». On ne vérifie pas les appels — on fournit juste des valeurs.
$taxRates = $this->createStub(TaxRateProvider::class); $taxRates->method('current')->willReturn(0.2); - Mock : un stub + la vérification des interactions (a-t-on appelé la méthode ? combien de fois ? avec quels arguments ?).
$mailer = $this->createMock(MailerInterface::class); $mailer->expects(self::once()) // doit être appelé exactement 1 fois ->method('send') ->with(self::isInstanceOf(Email::class)); $service = new BookingCreator(/* ... */, $mailer); $service->create($input); // PHPUnit échoue si send() n'a pas été appelé exactement une fois
| Stub | Mock | |
|---|---|---|
| But | fournir des valeurs | vérifier des interactions |
| Vérifie les appels | non | oui (expects) |
| Quand | dépendance-source de données | dépendance-effet (mailer, bus…) |
⚠️ Piège du sur-mock : mocker tout rend les tests fragiles (ils vérifient l’implémentation — « telle méthode a été appelée » — plutôt que le résultat). Un refactor interne les casse alors même que le comportement est correct. Préférez tester les résultats (avec des stubs pour les entrées) et ne mockez (avec
expects) que les effets de bord qui comptent vraiment (un e-mail a été envoyé). Trop de mocks = tests qui testent le mock.
3. Simuler les API externes
Pour tester du code qui appelle WordPress/Moodle/Stripe (Partie 13), on utilise MockHttpClient — aucune requête réseau :
use Symfony\Component\HttpClient\{MockHttpClient, Response\MockResponse};
$mock = new MockHttpClient([
new MockResponse(json_encode([['id' => 1, 'title' => 'Cours A']]), ['http_code' => 200]),
]);
$moodle = new MoodleClient($mock, 'fake-token');
$courses = $moodle->getCourses();
self::assertSame('Cours A', $courses[0]['title']); // déterministe, hors ligneLes tests restent rapides, déterministes et isolés des services tiers (qui peuvent être lents, down, ou coûteux).
4. Data providers : un test, plusieurs cas
Pour vérifier une logique sur plusieurs entrées sans dupliquer le test, on utilise un data provider :
use PHPUnit\Framework\Attributes\DataProvider;
final class PriceCalculatorTest extends TestCase
{
#[DataProvider('priceCases')]
public function testTotal(int $amount, float $rate, int $expected): void
{
$taxRates = $this->createStub(TaxRateProvider::class);
$taxRates->method('current')->willReturn($rate);
self::assertSame($expected, (new PriceCalculator($taxRates))->total($amount));
}
public static function priceCases(): array
{
return [
'TVA 20%' => [1000, 0.20, 1200],
'TVA 5.5%' => [1000, 0.055, 1055],
'sans taxe' => [1000, 0.0, 1000],
'montant nul'=> [0, 0.20, 0],
];
}
}Le test s’exécute une fois par cas, avec un nom lisible (les clés du tableau). Un cas qui échoue est immédiatement identifiable. C’est le moyen le plus efficace de couvrir des tables de vérité (règles de calcul, transitions d’état).
💡 Pour un dev Next.js : factories Foundry ≈ factory-bot +
@faker-js/faker;createStub/createMock≈vi.fn()/vi.spyOn();MockHttpClient≈msw(mock des requêtes) ;#[DataProvider]≈test.eachde Vitest/Jest. Vous retrouvez tous vos outils, avec les mêmes bons réflexes (tester les résultats, mocker parcimonieusement).
✏️ Exercices
Exercice 1. Utilisez une factory pour créer 3 réservations confirmées dans un test.
✅ Solution
BookingFactory::createMany(3, ['status' => BookingStatus::Confirmed]);Foundry crée et persiste 3 réservations avec les valeurs par défaut de la factory, sauf status surchargé. Les autres champs (email, date, places) sont générés par Faker.
Exercice 2. Quand utiliser un mock avec expects() plutôt qu’un simple stub ?
✅ Solution
Quand on veut vérifier une interaction (un effet de bord) plutôt que juste fournir une valeur : par exemple s’assurer qu’un e-mail a été envoyé ($mailer->expects(self::once())->method('send')) ou qu’un message a été dispatché. Pour une dépendance qui ne fait que fournir des données (un provider de taux), un stub suffit. On évite de sur-mocker pour ne pas coupler les tests à l’implémentation.
Exercice 3. Vous testez une fonction de transition d’état avec 6 combinaisons entrée→sortie. Quelle technique évite d’écrire 6 tests quasi identiques ?
✅ Solution
Un data provider (#[DataProvider]) : une méthode statique renvoie les 6 cas [entrée..., attendu] (avec des clés lisibles), et un seul test s’exécute une fois par cas. Couverture large, code minimal, et chaque cas échoué est identifiable par son nom. C’est l’équivalent de test.each.
🧠 Quiz de révision
1. Qu’apporte une factory Foundry par rapport à des fixtures statiques ?
Des factories expressives avec Faker intégré (createOne/createMany), où l’on surcharge juste les champs qui comptent — plus lisible et flexible pour préparer l’état d’un test.
2. Différence entre stub et mock ?
Un stub fournit des valeurs prédéfinies (pas de vérification d’appel) ; un mock vérifie en plus les interactions (expects, arguments, nombre d’appels).
3. Pourquoi éviter de tout mocker ?
Parce que trop de mocks testent l’implémentation (« telle méthode appelée ») et deviennent fragiles au refactor. On préfère tester les résultats, en ne mockant que les effets de bord importants.
4. Comment teste-t-on du code appelant une API externe ?
Avec MockHttpClient (+ MockResponse) : réponses simulées, sans réseau — rapide et déterministe.
5. À quoi sert un data provider ?
À exécuter un même test avec plusieurs jeux d’entrées (#[DataProvider]), pour couvrir de nombreux cas sans dupliquer le code.
Prochain chapitre : 14.5 — PHPStan — l’analyse statique qui attrape les bugs sans exécuter le code.