Skip to Content
SymfonyPartie 14 — Tests & qualité14.4 — Fixtures, mocks & data providers

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(...)).
  • MockHttpClient simule 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
StubMock
Butfournir des valeursvérifier des interactions
Vérifie les appelsnonoui (expects)
Quanddépendance-source de donnéesdé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 ligne

Les 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/createMockvi.fn() / vi.spyOn() ; MockHttpClientmsw (mock des requêtes) ; #[DataProvider]test.each de 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.