« Une dernière chose... »
Dans un manoir sur un serveur web en production, chaque bug est un crime. Une NullPointerException a été retrouvée dans la bibliothèque. La victime ? L'expérience utilisateur.
Qui est le coupable ? Le Service ? Le Controller ? La Base de Données ? Pour le savoir, nous allons mener l'enquête avec PHPUnit. Sortez votre loupe !
L'interrogatoire : Le test unitaire
Le test unitaire, c'est l'interrogatoire du suspect dans une salle blanche, isolée, sans fenêtre.
Le suspect, c'est votre classe PHP (ici, PostNavigationService).
Pour qu'il ne puisse pas rejeter la faute sur les autres, je vais remplacer ses complices par des doublures (mocks).
La scène
Le service src/Service/Content/PostNavigationService.php doit trouver des articles similaires.
Il a deux complices :
MeilisearchSearchService: le moteur de recherche.PostRepository: la base de données.
Dans mon test unitaire, je ne vais jamais appeler la vraie base de données ou le vrai Meilisearch, je vais simuler leur comportement.
La Preuve
// tests/Unit/Service/Content/PostNavigationServiceTest.php
public function testGetRelatedPostsUsesMeilisearchFirst(): void
{
// 1. La Préparation (Arrange)
// On crée les doublures. Ce sont des acteurs qui suivent un script.
/** @var PostRepositoryInterface&MockObject $repo */
$repo = $this->createMock(PostRepositoryInterface::class);
/** @var MeilisearchSearchService&MockObject $search */
$search = $this->createMock(MeilisearchSearchService::class);
// On isole le suspect avec ses faux complices
$service = new PostNavigationService($repo, $search);
// 2. Le Script (Expectations)
// On dit à l'acteur Meilisearch : "Si on t'appelle, dis que tu es disponible."
$search->expects($this->once())
->method('isAvailable')
->willReturn(true);
// "Et retourne ces deux articles fictifs."
$related1 = $this->createPost('Article A');
$related2 = $this->createPost('Article B');
$search->expects($this->once())
->method('search')
->willReturn([$related1, $related2]);
// 3. L'Action (Act)
$results = $service->getRelatedPosts($this->createPost('Current'), 3);
// 4. Le Verdict (Assert)
// Le service a-t-il bien renvoyé ce que Meilisearch lui a donné ?
$this->assertCount(2, $results);
$this->assertSame([$related1, $related2], $results);
}
Si ce test échoue, on sait à 100% que le coupable est PostNavigationService. Ce n'est pas la faute du réseau, ni de la BDD, car ils n'étaient pas là.
La reconstitution : le test fonctionnel
Le test fonctionnel, c'est la reconstitution du crime sur les lieux.
Ici, on ne mocke (presque) rien. On démarre le vrai framework Symfony (le Kernel), la vraie base de données de test, et on simule une requête HTTP : c'est l'inspecteur qui se fait passer pour un client.
La preuve
// tests/Functional/Controller/Content/PostActionTest.php
public function testPostPageLoadsSuccessfully(): void
{
// 1. On démarre le client (le navigateur simulé)
$client = self::createClient();
// 2. On récupère un vrai article en base de test
// (Note: La BDD a été peuplée par des Fixtures au préalable)
$postRepository = self::getContainer()->get(PostRepository::class);
$post = $postRepository->findOneBy([]);
// 3. On tente d'accéder à la page
$client->request('GET', sprintf('/billets/%s', $post->slug));
// 4. On vérifie que la porte s'ouvre (200 OK)
$this->assertResponseIsSuccessful();
// 5. On vérifie qu'on est au bon endroit
$this->assertSelectorTextContains('h1', $post->title);
}
Ici, on teste l'intégration : Le Routeur -> Le Controller -> Le Service -> Twig. Si ça plante, le coupable peut être n'importe où dans la chaîne. C'est moins précis, mais plus réaliste.
La caméra de surveillance : le test E2E
Enfin, il y a le test End-to-End : c'est l'artillerie lourde.
Avec un test E2E, on piloterait un vrai navigateur (Chrome/Firefox) via une librairie comme Symfony Panther. On clique sur les vrais boutons, le JavaScript s'exécute, les CSS sont rendus.
Pourquoi ne pas l'utiliser sur ce projet ? Le coût. Un test E2E est lent (plusieurs secondes) et fragile (un changement de CSS peut tout casser). Pour un blog technique comme le mien, mes tests fonctionnels (qui simulent le navigateur sans l'interface graphique) suffisent à couvrir 99% des cas en quelques millisecondes. On réserve le FBI pour les applications riches où le JavaScript est roi.
Le mot de la fin
Une bonne stratégie de test ressemble à une pyramide :
- Beaucoup de Tests Unitaires avec des interrogatoires : ils sont rapides (ms), précis et couvrent tous les cas tordus. C'est votre filet de sécurité quotidien.
- Quelques Tests Fonctionnels avec des reconstitutions : ils assurent que les composants se parlent bien entre eux (Controller + Template).
- Une poignée de Tests E2E avec des caméras : ils valident les parcours critiques (Paiement, Login).
N'attendez pas le crime pour enquêter. Soyez un détective proactif. Écrivez le test (l'accusation) avant le code (le crime). C'est le TDD (Test Driven Development ou Test Driven Detective).