Vous souhaitez nous soutenir ? Devenez sponsor de l'association sur notre page Github

PHPUnit : conseils et astuces qui nous ont vraiment aidés

Publié le 22 juin 2025 par Mathieu De Gracia
Couverture de l'article PHPUnit : conseils et astuces qui nous ont vraiment aidés

Tester son code est une chose essentielle pour s'assurer du bon fonctionnement et de l'absence de régressions au fil des développements : ils figent les comportements attendus et facilitent la détection des anomalies lors des évolutions.

Au fil des années et de l’utilisation de PHPUnit avec Laravel, nous accumulons des pratiques et des petits conseils pour des situations très spécifiques, voici un recueil d’une dizaine d’astuces qui pourraient vous être utiles !

assertEqualsCanonicalizing

Vérifier la forme d'un tableau peut être une chose pénible, surtout quand l'ordonnancement des valeurs est fluctuant, toutes les valeurs se trouvent dans votre tableau mais l'ordre est parfois difficile à prévoir provoquant des erreurs aléatoires.

1$this->assertSame(
2 [1,2,3],
3 [3,2,1],
4); // ❌

Mais parfois le sens des valeurs n’a pas d’importance particulière et peut être ignoré, la méthode assertEqualsCanonicalizing répond à ce besoin en vérifiant leur présence sans tenir compte de l’ordre !

1$this->assertEqualsCanonicalizing(
2 [1,2,3],
3 [3,2,1],
4); // ✅

Vérifier les valeurs dans un test HTTP

Tester la route suivante sera relativement simple, mais la vérification des variables injectées pourrait s’avérer un peu plus délicate :

1 
2namespace App\Http\Controllers;
3 
4class HomeController extends Controller
5{
6 public function __invoke()
7 {
8 return view('homepage', [
9 'title' => 'Home',
10 ]);
11 }
12}

Heureusement pour nous, l’élément response retourné par la méthode get est une instance de TestResponse disposant d’une méthode magique __get permettant d’accéder directement aux variables injectées dans la vue ;

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function i_am_a_random_test(): void
5 {
6 $response = $this->get('/');
7 
8 $response->assertSuccessful();
9 
10 $this->assertEquals('Home', $response['title']);
11 }
12}

Cette fonctionnalité n’est ni usuelle ni documentée par Laravel, dans la plupart des cas nous vous conseillons d’utiliser la méthode viewData qui offre un comportement similaire :

1$this->assertEquals('Home', $response->viewData('title'));

Lever et compter des assertions

Il arrive, rarement, que les assertions classiques de PHPUnit comme assertSame ne suffisent pas.

Dans ce cas, vous pouvez recourir à addToAssertionCount pour indiquer manuellement à PHPUnit qu'une assertion a eu lieu et la comptabiliser par la suite via numberOfAssertionsPerformed :

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function i_am_a_random_test(): void
5 {
6 $this->addToAssertionCount(1);
7 $this->addToAssertionCount(1);
8 
9 $this->assertEquals(2, $this->numberOfAssertionsPerformed());
10 // ✅
11 }
12}

Cette situation est exceptionnelle, pour l'avoir rencontrée en production qu'une seule et unique fois, mais cela pourrait ponctuellement vous aider et débloquer un test récalcitrant !

Attention cependant, le nombre d'assertions déclenchées manuellement à l'aide de addToAssertionCount ne s'additionnera pas aux autres méthodes d'assertions !

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function i_am_a_random_test(): void
5 {
6 $this->addToAssertionCount(1);
7 
8 $this->assertTrue(true);
9 
10 $this->assertEquals(2, $this->numberOfAssertionsPerformed());
11 // ❌ Failed asserting that 1 matches expected 2.
12 }
13}

DoesNotPerformAssertions

Au contraire, il est parfois possible que vos tests ne vérifient aucune assertion, la seule bonne exécution du code, sans lever d'erreur, étant un signe suffisant de son bon fonctionnement.

Cependant, PHPUnit est configuré de sorte (et à raison) qu'un test ne validant aucune assertion soit considéré comme étant une anomalie et déclenchera un warning :

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function i_am_a_random_test(): void
5 {
6 // ⚠️ This test did not perform any assertions
7 }
8}

Si cette situation est légitime dans vos tests, vous pouvez facilement modifier ce comportement en précisant à PHPUnit qu'il est normal que le test ne léve aucune assertion à l'aide de l'attribut expectNotToPerformAssertions :

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 #[\PHPUnit\Framework\Attributes\DoesNotPerformAssertions]
5 public function i_am_a_random_test(): void
6 {
7 // ✅
8 }
9}

RunInSeparateProcess

Par défaut, lorsque vous exécutez PHPUnit, tous les tests s'exécutent dans le même processus. Cela signifie que les données globales sont partagées entre les tests ce qui peut parfois provoquer des conflits.

Par exemple, si un premier test définit une constante, un second test ne pourra pas la redéfinir à nouveau, cela étant interdit par PHP :

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function first_test_define_constant(): void
5 {
6 define('TEST_CONSTANT', 'first');
7 
8 $this->assertTrue(true);
9 }
10 
11 #[\PHPUnit\Framework\Attributes\Test]
12 public function second_test_define_constant(): void
13 {
14 define('TEST_CONSTANT', 'second');
15 // ❌ Constant TEST_CONSTANT already defined
16 
17 $this->assertTrue(true);
18 }
19}

Pour répondre à cette problématique, il est possible de préciser à PHPUnit d'exécuter un test dans un processus dédié à l'aide de l'attribut RunInSeparateProcess :

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 #[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
5 public function first_test_define_constant(): void
6 {
7 define('TEST_CONSTANT', 'first');
8 
9 $this->assertTrue(true);
10 }
11 
12 #[\PHPUnit\Framework\Attributes\Test]
13 public function second_test_define_constant(): void
14 {
15 define('TEST_CONSTANT', 'second'); // ✅
16 
17 $this->assertTrue(true);
18 }
19}

Désormais les deux tests fonctionnent, car le premier, définissant la constante, se trouve dans un processus dédié qui n'impactera pas l'exécution du second test.

Les seeders

Dans certains tests, il peut être nécessaire de lancer des seeders pour inserer les données essentielles en base de données.

Si cela n’est pas configuré en amont, vous pouvez demander à Laravel d’exécuter le seeder par défaut (DatabaseSeeder) dans un test en ajoutant simplement la propriété $seed à ce dernier :

1class ExampleTest extends TestCase
2{
3 public $seed = true;
4}

En plus de cette propriété, vous pouvez également spécifier le seeder à utiliser en précisant la propriété $seeder :

1class ExampleTest extends TestCase
2{
3 public $seed = true;
4 public $seeder = UserSeeder::class;
5}

Simuler une page précédente

Dans le cadre de vos tests HTTP, il peut parfois arriver que certaines actions déclenchent une redirection vers la page précédente, cependant, dans un test fonctionnel, cette page "précédente" n'existe pas réellement car le test simule un utilisateur sans historique de navigation.

Cela peut donc poser problème ... à moins d'explicitement renseigner l'origine de la requête à l’aide de la méthode from !

1class ExampleTest extends TestCase
2{
3 #[\PHPUnit\Framework\Attributes\Test]
4 public function i_am_a_random_test(): void
5 {
6 $response = $this
7 ->from('admin/users')
8 ->post('admin/users/store');
9 
10 $response->assertRedirect('admin/users');
11 }
12}

De cette facon, Laravel considèrera que l'URL renseignée dans le from fut la page appelante lors du call vers admin/users/store et redirigera au besoin vers cette dernière.

Marquer manuellement les tests

Vos tests peuvent parfois se comporter bizarrement et générer des faux négatifs, plutôt que de supprimer ces tests "flaky", il est possible de les ignorer temporairement en utilisant la méthode markTestSkipped :

1class ExampleTest extends TestCase
2{
3 public function test_one(): void
4 {
5 $this->markTestSkipped('test are skipped because ...');
6 
7 $this->assertTrue(true);
8 }
9}

Ces tests apparaîtront toujours dans l’output de PHPUnit mais ne seront plus exécutés, vous permettant d’y revenir plus tard pour les corriger.

De facon similaire, vous pouvez marquer un test comme incomplet avec la méthode suivante :

1$this->markTestIncomplete('test are incomplete but not in error');

Un test incomplet est une chose peu courante dans PHPUnit, il représente un test qui ne peut être exécuté, le plus souvent pour des raisons techniques ou parce que la fonctionnalité n’est pas encore prête, mais ne devant pour autant pas être considéré comme une erreur.

Dans un tout autre registre, la méthode fail permet d’interrompre brutalement l’exécution d’un test et de le marquer comme échoué :

1$this->fail('test are failed !');

Lancer les tests aléatoirement

Un bon ensemble de tests se doit être déterministe et ne pas subir d’effets de bord provoqués par l’exécution d’autres tests. Pour s’en assurer, vous pouvez lancer vos tests dans un ordre aléatoire à l’aide de l’option suivante :

1phpunit --order-by=random

Cela permettra de détecter les dépendances implicites entre vos tests qui seront souvent sources de comportements instables.

De plus, il est possible de spécifier un seed afin de rejouer exactement le même ordre aléatoire, notamment pour reproduire un échec :

1phpunit --order-by=random --random-order-seed=1234

Tester un middleware

Les middlewares sont des composants essentiels de vos applications Laravel, placés en amont des routes et des controllers, un bug à ce niveau pourrait fortement impacter l’expérience utilisateur.

Il est donc crucial de ne pas négliger leur test, même s’ils peuvent paraître complexes à manipuler, étant généralement instanciés par le framework, un middleware reste une classe comme une autre ... rien ne vous empêche de l’instancier manuellement dans vos tests !

1 
2namespace Tests\Feature;
3 
4use Tests\TestCase;
5use Illuminate\Http\Request;
6use Illuminate\Http\Response;
7use App\Http\Middleware\MyMiddleware;
8 
9class ExampleTest extends TestCase
10{
11 #[\PHPUnit\Framework\Attributes\Test]
12 public function i_am_a_random_test(): void
13 {
14 $middleware = new MyMiddleware();
15 
16 $response = $middleware->handle(new Request(), function ($request) {
17 return new Response('Hello World');
18 });
19 
20 $this->assertEquals('Hello World', $response->getContent());
21 }
22}

Une seconde option consistera à définir artificiellement une route dans le test à laquelle vous attribuerez votre middleware, vous pourrez alors tester cette route comme n'importe quelle autre !

1 
2namespace Tests\Feature;
3 
4use Tests\TestCase;
5use Illuminate\Http\Response;
6use App\Http\Middleware\MyMiddleware;
7use Illuminate\Support\Facades\Route;
8 
9class ExampleTest extends TestCase
10{
11 #[\PHPUnit\Framework\Attributes\Test]
12 public function i_am_a_random_test(): void
13 {
14 Route::middleware(MyMiddleware::class)->get('/', function () {
15 return new Response('Hello World');
16 });
17 
18 $this->get('/')->assertSee('Hello World');
19 }
20}

Corriger progressivement les tests

Inévitablement, vos tests finiront par planter un jour ou l’autre, que ce soit à la suite d’une refonte, d’un nouveau développement ou d’une mise à jour d’une dépendance.

Face à une marée d’erreurs, il peut être rassurant (et également plus rapide) de demander à PHPUnit de s’arrêter dès la première erreur rencontrée.

Cela vous permettra de vous concentrer sur les anomalies une par une, sans subir une exécution longue et parfois inutile de l’ensemble de la suite de tests.

1phpunit --stop-on-failure --stop-on-error

Les erreurs et les échecs sont deux choses différentes avec PHPUnit : une erreur représente un plantage du code, tel qu'une erreur fatale, tandis qu’un échec est une assertion qui n'a pas pu être validée.

Regrouper les tests

Par moments, il peut être intéressant de regrouper certains tests sous un même tag afin de les inclure ou de les exclure lors de l'exécution des tests.

Par exemple, si certains de vos tests échouent occasionnellement sans raison claire, vous pouvez utiliser l’attribut Group pour les marquer comme flaky :

1#[\PHPUnit\Framework\Attributes\Test]
2#[\PHPUnit\Framework\Attributes\Group('flaky')]
3public function i_am_a_random_test(): void
4{
5 // this test fails occasionally 🤕
6}

Vous pourrez ensuite facilement les exclure lors de l’exécution de PHPUnit :

1vendor/bin/phpunit --exclude-group flaky

Ou au contraire, exécuter uniquement les tests associés à un tag donné :

1vendor/bin/phpunit --group flaky

Les groupes sont pratiques pour organiser vos tests en fonction de critères communs : les tests rapides, les tests lents, ceux qui font appel à des APIs externes, ou encore ceux associés à un module ou une couche spécifique.

Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie