Makina Blog
Scénario complexe dans PHPUnit et ordre d'exécution des tests
J'ai récemment (re)découvert une fonctionnalité suffisamment banale et simple de PHPUnit pour qu'on oublie qu'elle existe lorsque nous n'avons plus besoin d'elle.
Retour sur ma découverte et la documentation liée !
Le use case
Pour commencer, lorsque vous écrivez une classe de tests, telle que celle-ci :
<?php
declare (strict_types=1);
namespace UberProject\Tests;
use PHPUnit\Framework\TestCase;
class SomeDomainFunctionalTest extends TestCase
{
private ?SuperbeObject $superbeObject;
public function testQueCaFaitCa(): void
{
$this->superbeObject = my_object_factory();
$this->superbeObject->declencheUnTraitementMetierQuiPeutEchouer();
self::assertTrue(true);
}
public function testQuEnsuiteCaFaitCa(): void
{
$this->superbObject->puisUnAutreQuiEnDepend();
self::assertFalse(false);
}
}
Vous pouvez tout simplement :
vendor/bin/phpunit --filter=SomeDomainFunctionalTest --stop-on-failure
Et apprécier le joli résultat vert de PHPUnit nous indiquant que tout a fonctionné.
Pour que nous soyons bien tous en phase, ce test est bancal et ne marche que par accident :
- Ici nous comptons sur l'état d'un objet à la suite du premier
testQueCaFaitCa()
pour continuer le scénario sur le deuxièmetestQuEnsuiteCaFaitCa()
. - Pour que ça fonctionne, il faut que les méthodes soient exécutées dans l'ordre.
- Pour préparer un scénario, il faudrait mieux utiliser la méthode
setUp()
, mais elle ne permet pas de conserver un état entre les tests.
Par le plus grand des hasards - mais rassurez-vous c'est déterminé, pas aléatoire − les tests testQueCaFaitCa()
et testQuEnsuiteCaFaitCa()
sont exécutés dans l'ordre dans lequel vous les avez écrits. Et ça, bien sûr, si vous n'avez pas installé de plugins étranges de PHPUnit qui permettraient, par exemple d'exécuter les tests en parallèle, ou tout simplement si vous utilisez l'une de ces options de PHPUnit : --order-by
ou --process-isolation
.
Que se passe-t-il si je trouve un bug dans le second test et souhaite n'exécuter que ce dernier ? Et bien nous allons lancer :
vendor/bin/phpunit --filter=SomeDomainFunctionalTest::testQuEnsuiteCaFaitCa --stop-on-failure
Vous vous attendez à des applaudissements ? Et bien, il y a peu de temps pendant une longue phase de débug dans un projet, je m'attendais à en recevoir. Seulement personne n'a applaudi, à l'exception de PHPUnit qui a explosé au vol laissant mon écran dans un état proche de celui d'une belle nuit d'été un soir de feux d'artifice.
Et bien oui, parce que ce scénario, si je lui demande d'exécuter ma seconde méthode sans la première, n'aura pas de $superbeObject
et donc ne pourra pas marcher.
La solution
Après quelques heures de débug, j'ai fini par me rendre compte que cette classe de tests de quelques milliers de lignes de code était bel et bien un scénario dans lequel chaque méthode dépendait du résultat d'une autre. Et c'est là que la révélation apparaît : la personne ayant écrit ce test à l'origine avait tout simplement oublié l'existence de l'annotation @depends
de PHPUnit.
Quel outil merveilleux, j'en avais complètement oublié l'existence : le seul moyen qui assure que des tests soient réellement exécutés dans le bon ordre c'est de le demander explicitement. De plus, en déclarant ce lien de dépendance explicitement, si un test dans la chaîne échoue, PHPUnit considèrera alors que les pré-conditions pour exécuter les suivants ne sont pas remplies, et agrémentera votre console d'un magnifique S
comme Skipped
, avec un message clair et limpide si vous activez la verbosité à l'exécution.
Mais nous allons voir que cela apporte aussi d'autres éléments assez pratiques.
Ré-écrivons donc notre test :
<?php
declare (strict_types=1);
namespace UberProject\Tests;
use PHPUnit\Framework\TestCase;
class SomeDomainFunctionalTest extends TestCase
{
public function testQueCaFaitCa(): void
{
$superbeObject = my_object_factory();
$superbeObject->declencheUnTraitementMetierQuiPeutEchouer();
self::assertTrue(true);
return $superbeObject;
}
/**
* @depends testQueCaFaitCa()
*/
public function testQuEnsuiteCaFaitCa(SuperbeObject $superbeObject): void
{
$superbeObject->puisUnAutreQuiEnDepend();
self::assertFalse(false);
}
}
Et pour lancer tout ça :
vendor/bin/phpunit --filter=SomeDomainFunctionalTest --stop-on-failure -vv
Oui, j'ai rajouté -vv
pour le plaisir.
Vous remarquerez ce @depends testQueCaFaitCa()
qui implique donc :
testQueCaFaitCa()
ettestQuEnsuiteCaFaitCa()
seront exécutées dans cet ordre.- Si
testQueCaFaitCa()
échoue,testQuEnsuiteCaFaitCa()
ne sera pas exécutée. - Le premier paramètre de
testQuEnsuiteCaFaitCa()
devient alors le retour de la méthodetestQueCaFaitCa()
.
Mais par contre, ceci a un effet de bord qui parfois peut être génant :
vendor/bin/phpunit --filter=SomeDomainFunctionalTest::testQuEnsuiteCaFaitCa --stop-on-failure -vv
# ... de l'output...
This test depends on "UberProject\Tests\SomeDomainFunctionalTest::testQueCaFaitCa" to pass.
Au moins le message est clair. Pour palier à ce petit défaut, je vous conseille tout simplement de créer une classe de tests par scénario, ou d'utiliser d'autres annotations qui permettent de lancer les tests par groupes, tels que @covers
, @uses
ou @group
.
Conclusion
N'oubliez jamais cette annotation @depends
, gardez-la dans un coin de cerveau, nous ne l'utilisons finalement que rarement, mais elle est géniale !
Actualités en lien

Créer une application Symfony/Vue.js
Pour faire des essais ou bien démarrer un nouveau projet, vous avez besoin de créer rapidement une application Symfony couplée avec un front Vue.js ? Suivez le guide !

Comment démarrer un projet Symfony 5 en 5 minutes ?
Depuis quelques versions, le framework Symfony fournit de nombreux outils pour bâtir très rapidement une application fonctionnelle. Voyons ce qu'on peut faire en 5 minutes.

Symfony : utiliser une contrainte de type Callback dans un formulaire pour de la validation spécifique
Vous devez développer une contrainte pour un formulaire métier ? La déclarer à l'aide du composant Validation de Symfony est peut-être excessif : il est aussi possible de le faire en passant par une assertion de type Callback.