Chapitre 14.2 — Tests fonctionnels (WebTestCase)
Partie 14 : Tests & qualité — Chapitre 2/7 Version de référence : Symfony 7.4.
⏱️ TL;DR
WebTestCasesimule de vraies requêtes HTTP (sans serveur) :static::createClient(),$client->request('GET', '/...'), puis des assertions sur la réponse.KernelTestCase(dont hérite WebTestCase) démarre le kernel et donne accès au conteneur (static::getContainer()) pour récupérer des services/le repository.- Assertions dédiées :
assertResponseIsSuccessful(),assertResponseStatusCodeSame(404),assertSelectorTextContains('h1', '…'),assertResponseRedirects('/login').- Le crawler permet de cliquer des liens et soumettre des formulaires comme un utilisateur.
- Base de test dédiée ; on isole chaque test (rollback via DAMADoctrineTestBundle).
- S’authentifier :
$client->loginUser($user).- Pour un dev Next.js : c’est un test d’intégration façon supertest — on tape des routes et on vérifie le HTML/le statut, sans lancer de serveur réel.
🎯 Objectifs
- Écrire un test fonctionnel qui requête une route et vérifie la réponse.
- Accéder aux services et à la base dans un test.
- Cliquer des liens et soumettre des formulaires.
- Authentifier un utilisateur et isoler la base.
1. KernelTestCase vs WebTestCase
KernelTestCasedémarre le kernel Symfony : on accède au conteneur et donc à tous les services (repository, EntityManager, un service métier). Idéal pour tester une intégration avec la base sans passer par HTTP.WebTestCaseajoute un client HTTP simulé : on envoie des requêtes et on vérifie les réponses (statut, HTML, redirection). C’est le test fonctionnel de bout en bout côté serveur.
// tests/Functional/HomeControllerTest.php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class HomeControllerTest extends WebTestCase
{
public function testHealthEndpointReturnsOk(): void
{
$client = static::createClient();
$client->request('GET', '/api/health');
self::assertResponseIsSuccessful(); // 2xx
self::assertJson($client->getResponse()->getContent()); // c'est du JSON
}
public function testHomePageShowsTitle(): void
{
$client = static::createClient();
$client->request('GET', '/');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'BookLab'); // le HTML contient <h1>BookLab</h1>
}
}2. Les assertions de réponse
| Assertion | Vérifie |
|---|---|
assertResponseIsSuccessful() | statut 2xx |
assertResponseStatusCodeSame(404) | statut exact |
assertResponseRedirects('/login') | redirection vers |
assertSelectorTextContains('h1', 'x') | texte d’un sélecteur CSS |
assertSelectorExists('.flash-success') | présence d’un élément |
assertResponseHeaderSame('Content-Type', '…') | en-tête |
assertJson($content) | contenu JSON valide |
Ces assertions (fournies par le pont Symfony) rendent les tests lisibles et expressifs.
3. Accéder aux services et à la base
Dans un test, on récupère un service via le conteneur de test :
$client = static::createClient();
$repository = static::getContainer()->get(BookingRepository::class);
$count = $repository->count([]);
self::assertGreaterThanOrEqual(0, $count);static::getContainer() donne accès au conteneur (en test, même les services privés sont accessibles). On l’utilise pour préparer des données, vérifier l’état de la base après une action, ou tester un service directement.
4. Cliquer et soumettre (le crawler)
Le crawler simule les interactions d’un utilisateur :
public function testCreateBookingViaForm(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/admin/bookings/new');
// remplir et soumettre le formulaire
$form = $crawler->selectButton('Réserver')->form([
'booking[customerEmail]' => 'test@example.com',
'booking[seats]' => '2',
]);
$client->submit($form);
self::assertResponseRedirects('/admin/bookings'); // POST → Redirect
$client->followRedirect();
self::assertSelectorTextContains('.flash-success', 'créée');
}On peut aussi cliquer un lien : $client->click($crawler->selectLink('Modifier')->link()). C’est un test de bout en bout du parcours (route → contrôleur → formulaire → validation → redirection → flash).
5. Authentifier un utilisateur
Pour tester des zones protégées, on connecte un utilisateur sans passer par le formulaire de login :
$client = static::createClient();
$user = static::getContainer()->get(UserRepository::class)->findOneBy(['email' => 'admin@booklab.test']);
$client->loginUser($user); // simule l'authentification
$client->request('GET', '/admin');
self::assertResponseIsSuccessful(); // l'admin a accèsloginUser() place l’utilisateur dans le contexte de sécurité — pratique pour tester rôles et voters (Partie 10).
6. La base de test et l’isolation
Les tests fonctionnels touchent la base : on utilise une base de test dédiée (environnement test, DATABASE_URL distincte, souvent SQLite ou un schéma séparé). Chaque test doit être isolé : ce qu’un test écrit ne doit pas polluer le suivant.
La solution standard : DAMADoctrineTestBundle, qui enveloppe chaque test dans une transaction et la rollback à la fin — la base revient à son état initial après chaque test, instantanément.
composer require --dev dama/doctrine-test-bundle⚠️ Piège isolation : sans rollback (ou sans re-création du schéma), les tests fonctionnels s’influencent : un test qui crée une réservation fait échouer un autre qui compte les réservations. C’est la cause n°1 de tests « qui passent seuls mais échouent ensemble » (flaky). DAMADoctrineTestBundle (transaction + rollback par test) règle ça proprement et rapidement. Préparez les données via fixtures (chapitre 14.4), pas en dépendant de l’ordre des tests.
💡 Pour un dev Next.js :
WebTestCase≈ supertest (taper des routes, asserter la réponse) + un crawler pour l’HTML. La grande commodité :loginUser()pour tester l’auth, etgetContainer()pour manipuler les services/la base — le tout sans serveur réel (le kernel est monté en mémoire). Rapide et fiable.
✏️ Exercices
Exercice 1. Écrivez un test vérifiant que GET /admin redirige vers le login pour un utilisateur non connecté.
✅ Solution
public function testAdminRequiresLogin(): void
{
$client = static::createClient();
$client->request('GET', '/admin');
self::assertResponseRedirects('/login'); // firewall → redirection vers la connexion
}(Suppose access_control: ^/admin ROLE_ADMIN et form_login sur le firewall, Partie 10.)
Exercice 2. Comment tester qu’un admin connecté accède bien à /admin ?
✅ Solution
Récupérer un User admin depuis le repository via static::getContainer(), le connecter avec $client->loginUser($user), puis $client->request('GET', '/admin') et assertResponseIsSuccessful(). loginUser() place l’utilisateur (avec ses rôles) dans le contexte de sécurité, sans passer par le formulaire.
Exercice 3. Vos tests fonctionnels « passent seuls mais échouent quand on lance toute la suite ». Cause probable et solution ?
✅ Solution
Défaut d’isolation : les tests partagent la base et se polluent (un test crée des données qui faussent un autre). Solution : DAMADoctrineTestBundle (chaque test dans une transaction rollbackée), une base de test dédiée, et des fixtures pour préparer un état connu — ne jamais dépendre de l’ordre des tests. C’est la cause classique des tests flaky.
🧠 Quiz de révision
1. Que fait WebTestCase de plus que KernelTestCase ?
Il ajoute un client HTTP simulé pour envoyer des requêtes et asserter les réponses (statut, HTML, redirection), en plus de l’accès au conteneur.
2. Comment accède-t-on à un service dans un test ?
Via static::getContainer()->get(Service::class) (les services privés sont accessibles en test).
3. Comment soumet-on un formulaire dans un test ?
Avec le crawler : $crawler->selectButton('...')->form([...]) puis $client->submit($form).
4. Comment authentifier un utilisateur en test ?
$client->loginUser($user) — place l’utilisateur dans le contexte de sécurité sans formulaire.
5. Comment isole-t-on les tests qui touchent la base ?
Avec DAMADoctrineTestBundle (transaction + rollback par test) + une base de test dédiée + des fixtures (pas de dépendance à l’ordre).
Prochain chapitre : 14.3 — ApiTestCase — tester l’API (statuts, JSON, sécurité).