Chapitre 2. Écrire des tests pour PHPUnit

Exemple 2.1, « Tester des opérations de tableau avec PHPUnit » montre comment nous pouvons écrire des tests en utilisant PHPUnit pour contrôler les opérations PHP sur les tableaux. L'exemple introduit les conventions et les étapes de base pour écrire des tests avec PHPUnit:

  1. Les tests pour une classe Class vont dans une classe ClassTest.

  2. ClassTest hérite (la plupart du temps) de PHPUnit\Framework\TestCase.

  3. Les tests sont des méthodes publiques qui sont appelées test*.

    Alternativement, vous pouvez utiliser l'annotation @test dans le bloc de documentation d'une méthode pour la marquer comme étant une méthode de test.

  4. A l'intérieur des méthodes de test, des méthodes d'assertion telles que assertEquals() (voir Annexe A, Assertions) sont utilisées pour affirmer qu'une valeur constatée correspond à une valeur attendue.

Exemple 2.1. Tester des opérations de tableau avec PHPUnit

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertEquals(0, count($stack));

        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertEquals(1, count($stack));

        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}
?>


 

A chaque fois que vous avez la tentation de saisir quelque chose dans une instruction print ou dans une expression de débogage, écrivez le plutôt dans un test.

 
 --Martin Fowler

Dépendances des tests

 

Les tests unitaires sont avant tout écrits comme étant une bonne pratique destinée à aider les développeurs à identifier et corriger les bugs, à refactoriser le code et à servir de documentation pour une unité du logiciel testé. Pour obtenir ces avantages, les tests unitaires doivent idéalement couvrir tous les chemins possibles du programme. Un test unitaire couvre usuellement un unique chemin particulier d'une seule fonction ou méthode. Cependant, une méthode de test n'est pas obligatoirement une entité encapsulée et indépendante. Souvent, il existe des dépendances implicites entre les méthodes de test, cachées dans l'implémentation du scénario d'un test.

 
 --Adrian Kuhn et. al.

PHPUnit gère la déclaration de dépendances explicites entre les méthodes de test. De telles dépendances ne définissent pas l'ordre dans lequel les méthodes de test doivent être exécutées mais elles permettent de renvoyer une instance de la fixture de test par un producteur à des consommateurs qui en dépendent.

  • Un producteur est une méthode de test qui produit ses éléments testées comme valeur de retour.

  • Un consommateur est une méthode de test qui dépend d'un ou plusieurs producteurs et de leurs valeurs de retour.

Exemple 2.2, « Utiliser l'annotation @depends pour exprimer des dépendances » montre comment utiliser l'annotation @depends pour exprimer des dépendances entre des méthodes de test.

Exemple 2.2. Utiliser l'annotation @depends pour exprimer des dépendances

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>


Dans l'exemple ci-dessus, le premier test, testEmpty(), crée un nouveau tableau et affirme qu'il est vide. Le test renvoie ensuite la fixture comme résultat. Le deuxième test, testPush(), dépend de testEmpty() et reçoit le résultat de ce test dont il dépend comme argument. Enfin, testPop() dépend de testPush().

Note

La valeur de retour produite par un producteur est passée "telle quelle" à son consommateur par défaut. Cela signifie que lorsqu'un producteur renvoie un objet, une référence vers cet objet est passée a son consommateur. Lorsqu'une copie doit être utilisée au lieu d'une référence, alors @depends clone doit être utilisé au lieu de @depends.

Pour localiser rapidement les défauts, nous voulons que notre attention soit retenue par les tests en échecs pertinents. C'est pourquoi PHPUnit saute l'exécution d'un test quand un test dont il dépend a échoué. Ceci améliore la localisation des défauts en exploitant les dépendances entre les tests comme montré dans Exemple 2.3, « Exploiter les dépendances entre les tests ».

Exemple 2.3. Exploiter les dépendances entre les tests

<?php
use PHPUnit\Framework\TestCase;

class DependencyFailureTest extends TestCase
{
    public function testOne()
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

FS

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.


FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.


Un test peut avoir plusieurs annotations @depends. PHPUnit ne change pas l'ordre dans lequel les tests sont exécutés, vous devez donc vous assurer que les dépendances d'un test peuvent effectivement être utilisables avant que le test ne soit lancé.

Un test qui a plusieurs annotations @depends prendra une fixture du premier producteur en premier argument, une fixture du second producteur en second argument, et ainsi de suite. Voir Exemple 2.4, « Test avec plusieurs dépendances »

Exemple 2.4. Test avec plusieurs dépendances

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose MultipleDependenciesTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 3 assertions)


Fournisseur de données

Une méthode de test peut recevoir des arguments arbitraires. Ces arguments doivent être fournis par une méthode fournisseuse de données (additionProvider() dans Exemple 2.5, « Utiliser un fournisseur de données qui renvoie un tableau de tableaux »). La méthode fournisseuse de données à utiliser est indiquée dans l'annotation @dataProvider.

Une méthode fournisseuse de données doit être public et retourne, soit un tableau de tableaux, soit un objet qui implémente l'interface Iterator et renvoie un tableau pour chaque itération. Pour chaque tableau qui est une partie de l'ensemble, la méthode de test sera appelée avec comme arguments le contenu du tableau.

Exemple 2.5. Utiliser un fournisseur de données qui renvoie un tableau de tableaux

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Lorsque vous utilisez un grand nombre de jeux de données, il est utile de nommer chacun avec une clé en chaine de caractère au lieu de la valeur numérique par défaut. La sortie sera plus verbeuse car elle contiendra le nom du jeu de données qui casse un test.

Exemple 2.6. Utiliser un fournisseur de données avec des jeux de données nommés

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
Failed asserting that 2 matches expected 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Exemple 2.7. Utiliser un fournisseur de données qui renvoie un objet Iterator

<?php
use PHPUnit\Framework\TestCase;

require 'CsvFileIterator.php';

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return new CsvFileIterator('data.csv');
    }
}
?>
phpunit DataTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.

/home/sb/DataTest.php:11

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Exemple 2.8. La classe CsvFileIterator

<?php
use PHPUnit\Framework\TestCase;

class CsvFileIterator implements Iterator {
    protected $file;
    protected $key = 0;
    protected $current;

    public function __construct($file) {
        $this->file = fopen($file, 'r');
    }

    public function __destruct() {
        fclose($this->file);
    }

    public function rewind() {
        rewind($this->file);
        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

    public function valid() {
        return !feof($this->file);
    }

    public function key() {
        return $this->key;
    }

    public function current() {
        return $this->current;
    }

    public function next() {
        $this->current = fgetcsv($this->file);
        $this->key++;
    }
}
?>


Quand un test reçoit des entrées à la fois d'une méthode @dataProvider et d'un ou plusieurs tests dont il @depends, les arguments provenant du fournisseur de données arriveront avant ceux des tests dont il dépend. Les arguments des tests dépendants seront les mêmes pour chaque jeux de données Voir Exemple 2.9, « Combinaison de @depends et @dataProvider dans le même test »

Exemple 2.9. Combinaison de @depends et @dataProvider dans le même test

<?php
use PHPUnit\Framework\TestCase;

class DependencyAndDataProviderComboTest extends TestCase
{
    public function provider()
    {
        return [['provider1'], ['provider2']];
    }

    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
-    0 => 'provider1'
+    0 => 'provider2'
1 => 'first'
2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.


Note

Quand un test dépend d'un test qui utilise des fournisseurs de données, le test dépendant sera exécuté quand le test dont il dépend réussira pour au moins un jeu de données. Le résultat d'un test qui utilise des fournisseurs de données ne peut pas être injecté dans un test dépendant.

Note

Tous les fournisseurs de données sont exécutés avant le premier appel à la méthode statique setUpBeforeClass et le premier appel à la méthode setUp. De ce fait, vous ne pouvez accéder à aucune variable créée à ces endroits depuis un fournisseur de données. Ceci est requis pour que PHPUnit puisse calculer le nombre total de tests.

Tester des exceptions

Exemple 2.10, « Utiliser la méthode expectException() » montre comment utiliser la méthode expectException() pour tester si une exception est levée par le code testé.

Exemple 2.10. Utiliser la méthode expectException()

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


En complément à la méthode expectException() les méthodes expectExceptionCode(), expectExceptionMessage() et expectExceptionMessageRegExp() existent pour établir des attentes pour les exceptions levées par le code testé.

Alternativement, vous pouvez utiliser les annotations @expectedException, @expectedExceptionCode, @expectedExceptionMessage et @expectedExceptionMessageRegExp pour établir des attentes pour les exceptions levées par le code testé. Exemple 2.11, « Utiliser l'annotation @expectedException » montre un exemple.

Exemple 2.11. Utiliser l'annotation @expectedException

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Tester les erreurs PHP

Par défaut, PHPUnit convertit les erreurs, avertissements et remarques PHP qui sont émises lors de l'exécution d'un test en exception. En utilisant ces exceptions, vous pouvez, par exemple, attendre d'un test qu'il produise une erreur PHP comme montré dans Exemple 2.12, « Attendre une erreur PHP en utilisant @expectedException ».

Note

La configuration d'exécution PHP error_reporting peut limiter les erreurs que PHPUnit convertira en exceptions. Si vous rencontrez des problèmes avec cette fonctionnalité, assurez-vous que PHP n'est pas configuré pour supprimer le type d'erreurs que vous testez.

Exemple 2.12. Attendre une erreur PHP en utilisant @expectedException

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
      @expectedException PHPUnit\Framework\Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)


PHPUnit\Framework\Error\Notice et PHPUnit\Framework\Error\Warning représentent respectivement les remarques et les avertissements PHP.

Note

Vous devriez être aussi précis que possible lorsque vous testez des exceptions. Tester avec des classes qui sont trop génériques peut conduire à des effets de bord indésirables. C'est pourquoi tester la présence de la classe Exception avec @expectedException ou setExpectedException() n'est plus autorisé.

Quand les tests s'appuient sur des fonctions php qui déclenchent des erreurs comme fopen, il peut parfois être utile d'utiliser la suppression d'erreur lors du test. Ceci permet de contrôler les valeurs de retour en supprimant les remarques qui auraient conduit à une PHPUnit\Framework\Error\Notice de phpunit.

Exemple 2.13. Tester des valeurs de retour d'un code source qui utilise des erreurs PHP

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

?>
phpunit ErrorSuppressionTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)



Sans la suppression d'erreur, le test échouerait à rapporter fopen(/is-not-writeable/file): failed to open stream: No such file or directory.

Tester la sortie écran

Quelquefois, vous voulez confirmer que l'exécution d'une méthode, par exemple, produit une sortie écran donnée (via echo ou print, par exemple). La classe PHPUnit\Framework\TestCase utilise la fonctionnalité de mise en tampon de la sortie écran de PHP pour fournir la fonctionnalité qui est nécessaire pour cela.

Exemple 2.14, « Tester la sortie écran d'une fonction ou d'une méthode » montre comment utiliser la méthode expectOutputString() pour indiquer la sortie écran attendue. Si la sortie écran attendue n'est pas générée, le test sera compté comme étant en échec.

Exemple 2.14. Tester la sortie écran d'une fonction ou d'une méthode

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.


Tableau 2.1, « Méthodes pour tester les sorties écran » montre les méthodes fournies pour tester les sorties écran

Tableau 2.1. Méthodes pour tester les sorties écran

MéthodeSignification
void expectOutputRegex(string $regularExpression)Indique que l'on s'attend à ce que la sortie écran corresponde à une expression régulière $regularExpression.
void expectOutputString(string $attenduString)Indique que l'on s'attend que la sortie écran soit égale à une chaine de caractère $expectedString.
bool setOutputCallback(callable $callback)Configure une fonction de rappel (callback) qui est utilisée, par exemple, formater la sortie écran effective.
string getActualOutput()Renvoi la sortie écran courrante.


Note

En mode strict, un test qui produit une sortie écran échouera.

Sortie d'erreur

Chaque fois qu'un test échoue, PHPUnit essaie de vous fournir le plus de contexte possible pour identifier le problème.

Exemple 2.15. Sortie d'erreur générée lorsqu'un échec de comparaison de tableau

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Dans cet exemple, une seule des valeurs du tableau diffère et les autres valeurs sont affichées pour fournir un contexte sur l'endroit où l'erreur s'est produite.

Lorsque la sortie générée serait longue à lire, PHPUnit la divisera et fournira quelques lignes de contexte autour de chaque différence.

Exemple 2.16. Sortie d'erreur quand une comparaison de long tableaux échoue

<?php
use PHPUnit\Framework\TestCase;

class LongArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,  3, 4, 5, 6],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit LongArrayDiffTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
     13 => 2
-    14 => 3
+    14 => 33
     15 => 4
     16 => 5
     17 => 6
 )


/home/sb/LongArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Cas limite

Quand une comparaison échoue, PHPUnit crée une représentation textuel des valeurs d'entrées et les compare. A cause de cette implémentation un diff peut montrer plus de problèmes qu'il n'en existe réellement.

Cela arrive seulement lors de l'utilisation de assetEquals ou d'autres fonction de comparaison "faible" sur les tableaux ou les objets.

Exemple 2.17. Cas limite dans la génération de la différence lors de l'utilisation de comparaison faible

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )


/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.


Dans cet exemple, la différence dans le premier indice entre 1 et '1' est signalée même si AssertEquals considère les valeurs comme une correspondance.

Ouvrez un ticket sur GitHub pour proposer des améliorations à cette page. Merci!