Skip to Content
SymfonyPartie 14 — Tests & qualité14.1 — PHPUnit & tests unitaires

Chapitre 14.1 — PHPUnit & tests unitaires

Partie 14 : Tests & qualité — Chapitre 1/7 Version de référence : PHPUnit 11 / Symfony 7.4.

⏱️ TL;DR

  • PHPUnit est le framework de test de PHP. On l’installe via le test-pack ; les tests vivent dans tests/ (namespace App\Tests\).
  • Un test unitaire vérifie une unité isolée (un service, une méthode, un value object) sans base ni framework — on instancie la classe avec des doubles.
  • Structure : une classe …Test étend TestCase, des méthodes test…() avec des assertions (assertSame, assertTrue, expectException…).
  • On génère un squelette avec make:test, on lance avec php bin/phpunit.
  • Pyramide des tests : beaucoup d’unitaires (rapides), moins de fonctionnels, peu de bout-en-bout.
  • Pour un dev Next.js : PHPUnit ≈ Vitest/Jest ; les tests unitaires ciblent la logique pure, sans monter tout le framework.

🎯 Objectifs

  • Installer et lancer PHPUnit.
  • Écrire un test unitaire d’un service isolé.
  • Utiliser les assertions et tester les exceptions.
  • Comprendre la pyramide des tests.

1. Installer

composer require --dev symfony/test-pack

Le test-pack installe PHPUnit, le pont Symfony (dépréciations en test), browser-kit, css-selector (pour les tests fonctionnels, chapitre 14.2). La config est dans phpunit.dist.xml (environnement test, dossier tests/).

2. Un premier test unitaire

Un test unitaire vérifie une unité de logique en isolation — sans base, sans conteneur, sans HTTP. On instancie directement la classe testée.

// tests/Service/PriceCalculatorTest.php declare(strict_types=1); namespace App\Tests\Service; use App\Service\PriceCalculator; use App\Service\TaxRateProvider; use PHPUnit\Framework\TestCase; final class PriceCalculatorTest extends TestCase { public function testTotalAppliesTaxRate(): void { // un double du provider de taxe (pas de vraie dépendance) $taxRates = $this->createStub(TaxRateProvider::class); $taxRates->method('current')->willReturn(0.2); // 20 % $calculator = new PriceCalculator($taxRates); // 1000 centimes + 20 % = 1200 self::assertSame(1200, $calculator->total(1000)); } }

Points clés :

  • La classe étend TestCase (pas de kernel Symfony → rapide).
  • On injecte un double (createStub) pour la dépendance → on teste uniquement PriceCalculator.
  • Les méthodes de test commencent par test et utilisent des assertions (assertSame).

On génère un squelette avec :

php bin/console make:test # choisir "TestCase" pour un test unitaire

3. Les assertions courantes

AssertionVérifie
assertSame($a, $b)égalité stricte (===) — à préférer
assertEquals($a, $b)égalité lâche
assertTrue($x) / assertFalse($x)booléen
assertNull($x)est null
assertCount(3, $array)nombre d’éléments
assertInstanceOf(X::class, $obj)type
assertContains($v, $array)présence
assertStringContainsString('x', $s)sous-chaîne

Tester une exception :

public function testRejectsNegativeAmount(): void { $calculator = new PriceCalculator($this->createStub(TaxRateProvider::class)); $this->expectException(\InvalidArgumentException::class); $calculator->total(-10); // doit lever }

⚠️ Piège assertEquals vs assertSame : assertEquals compare de façon lâche ('1' == 1 passe) ; assertSame compare en strict (===). Préférez assertSame pour attraper les erreurs de type (un int attendu, une string reçue). C’est le même réflexe que === en PHP (chapitre 2.2).

4. Que tester en unitaire

Les tests unitaires ciblent la logique qui ne dépend pas de l’infrastructure :

  • services métier (calculs, règles) — le cœur de valeur ;
  • value objects et DTO (Money, DateRange…) ;
  • enums avec méthodes (chapitre 2.3) ;
  • voters (la logique d’autorisation, chapitre 10.2).

On isole les dépendances (base, HTTP, mailer) avec des doubles (chapitre 14.4). Ce qui touche la base ou l’HTTP réel relève des tests fonctionnels (14.2) ou d’API (14.3).

5. La pyramide des tests

  • Beaucoup de tests unitaires : rapides, ciblés, ils couvrent la logique métier.
  • Quelques tests fonctionnels/API : plus lents (kernel + base), ils vérifient l’intégration.
  • Peu de tests bout-en-bout : coûteux, réservés aux parcours critiques.

💡 Pour un dev Next.js : PHPUnit ≈ Vitest/Jest. La pyramide est la même qu’en JS (beaucoup d’unitaires, quelques tests d’intégration, peu d’E2E façon Playwright). Le réflexe « j’injecte des doubles pour isoler » (chapitre 5 : on injecte des interfaces) rend justement le code facile à tester — c’est la récompense du découplage. Un service bien conçu se teste sans monter le framework.

✏️ Exercices

Exercice 1. Écrivez un test unitaire vérifiant qu’une enum BookingStatus::Confirmed->isActive() renvoie true et que Cancelled->isActive() renvoie false.

✅ Solution

final class BookingStatusTest extends TestCase { public function testIsActive(): void { self::assertTrue(BookingStatus::Confirmed->isActive()); self::assertFalse(BookingStatus::Cancelled->isActive()); } }

Test pur, rapide, sans dépendance — idéal pour la logique d’une enum (chapitre 2.3).

Exercice 2. Pourquoi préférer assertSame à assertEquals ?

✅ Solution

assertSame compare en strict (===) : il attrape les erreurs de type ('1' vs 1, un objet vs un autre de même contenu). assertEquals compare de façon lâche et laisse passer des différences de type. Comme en PHP on privilégie === (chapitre 2.2), on privilégie assertSame pour des tests plus rigoureux.

Exercice 3. Un service BookingCreator dépend de l’EntityManager, du MessageBus et d’un MailerInterface. Peut-on le tester unitairement ? Comment isoler ?

✅ Solution

Oui, en isolant ses dépendances avec des doubles : un stub/mock de EntityManagerInterface, de MessageBusInterface, de MailerInterface, injectés dans le constructeur. On vérifie alors le comportement de BookingCreator (ex. « dispatch bien un message », « persiste l’entité ») sans base ni mail réels. Si la logique dépend fortement de la base (requêtes complexes), un test fonctionnel (14.2) avec une base de test peut être plus pertinent. C’est le découplage (injection d’interfaces) qui rend l’isolation possible.

🧠 Quiz de révision

1. Où vivent les tests et de quelle classe héritent les tests unitaires ?

Dans tests/ (namespace App\Tests\) ; les tests unitaires étendent TestCase (sans kernel Symfony).

2. Comment isole-t-on un service de ses dépendances ?

En injectant des doubles (createStub/createMock) à la place des vraies dépendances → on teste l’unité seule.

3. assertSame ou assertEquals ?

assertSame (strict, ===) de préférence, pour attraper les erreurs de type.

4. Comment teste-t-on qu’une méthode lève une exception ?

Avec $this->expectException(X::class) avant l’appel qui doit lever.

5. Que dit la pyramide des tests ?

Beaucoup d’unitaires (rapides), quelques fonctionnels/API (intégration), peu d’E2E (coûteux).


Prochain chapitre : 14.2 — Tests fonctionnels (WebTestCase) — simuler de vraies requêtes HTTP.