Makina Blog

Le blog Makina-corpus

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.

Le use case

Pour commencer, n'oubliez pas que 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 ensuite tout simplement la lancer comme ceci :

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ème testQuEnsuiteCaFaitCa().
  • 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éterministe, 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 des feux d'artifices ? Et bien, il y a peu de temps pendant une longue phase de débug dans un projet, j'y ai cru. Seulement la seule ayant explosé au vol est PHPUnit, 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() et testQuEnsuiteCaFaitCa() 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éthode testQueCaFaitCa().

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

Image
Encart blog DBToolsBundle
21/03/2024

L’ano­ny­mi­sa­tion sous stéroïdes avec le DBTools­Bundle

Le DbTools­Bundle permet d’ano­ny­mi­ser des tables d’un million de lignes en seule­ment quelques secondes. Cet article vous présente la métho­do­lo­gie mise en place pour arri­ver à ce résul­tat.

Voir l'article
Image
Encart article 2 : Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony
20/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (2/2)

Quels virages avons-nous pris après un premier projet expé­ri­men­tal pour stabi­li­ser notre concep­tion logi­cielle, et que ferons-nous plus tard ?

Voir l'article
Image
AFUP Meet-up DBToolsBundle
15/02/2024

Meetup AFUP Nantes de février : parlons anony­mi­sa­tion avec le DbTools­Bundle Symfony

Notre expert Symfony / PHP prend la parole au Meet-up de l’AFUP Nantes le 21 février pour présen­ter le nouveau bundle Symfony déve­loppé par Makina Corpus : le DbTools­Bundle !

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus