Les tests unitaires automatisés avec PHPUnit

Un test automatisé est un programme qui se découpe en trois phases dites AAA pour Arrange, Act, Assert.

Arrange

La mise en place de l’environnement : création et initialisation des objets nécessaires à l’exécution du test.

Act

Le test proprement dit.

Assert

La vérification des résultats obtenus par le test.

Le sous-système (l’ensemble des objets) éprouvé par le test est parfois appelé SUT (System Under Test).

PHPUnit

PHPUnit est un framework de tests unitaires pour PHP. Il s’inspire de JUnit, la version Java du framework.

PHPUnit fournit son propre exécutable phpunit pour exécuter les tests. Il fournit également une bibliothèque de classes nécessaire pour la rédaction des tests.

Structure d’une classe de test PHPUnit

Comme PHP est un langage orienté Objet, les tests PHPUnit sont regroupés dans des classes de test. Généralement, on groupe dans une classe les tests ayant la même classe comme point d’entrée et on nomme la classe de test à partir du nom de la classe testée suffixé par Test. Par exemple, pour tester la classe ConversionDate, on créera une classe ConversionDateTest.

Suffixer par Test le nom de la classe de test est juste une convention. Néanmoins, il est très fortement conseillé de la respecter car les outils utilisent également cette convention pour découvrir les classes de test à exécuter.

Les méthodes de test

Une classe de test est une classe qui hérite de la classe TestCase. Elle déclare des méthodes publiques sans paramètre et dont le nom commence par test. Ces méthodes ne doivent pas retourner de valeur particulière.

Note

Vous pouvez alternativement utiliser l’annotation @test au dessus de la déclaration d’une méthode de test. Ainsi, le format du nom de la méthode est libre.

Une méthode de test contient :

  • un ensemble d’instructions correspondant à la phase arrange (si nécessaire),

  • un ensemble d’instructions correspondant à la phase act (qui se limite généralement à l’appel de la méthode à tester),

  • un ensemble d’instructions correspondant à la phase assert.

L’exemple ci-dessous teste la fonction strtoupper qui renvoie une chaîne de caractères en majuscules :

Exemple d’une classe de test
<?php

use PHPUnit\Framework\TestCase;

class StringTest extends TestCase
{

    public function test_strtoupper_produit_une_chaine_en_majuscule()
    {
        // Bloc arrange
        $s = "Bonjour le monde";

        // Bloc act
        $maj = strtoupper($s);

        // Bloc assert
        $this->assertEquals("BONJOUR LE MONDE", $maj);
    }

}

Une méthode de test ne doit pas contenir d’instruction if ou switch puisqu’un test traduit un cas d’utilisation simple sans choix possible. De même, une méthode de test ne devrait contenir qu’exceptionnellement des boucles for ou while.

Les assertions

La classe TestCase fournit des méthodes pour déclarer des assertions. Ces méthodes permettent de vérifier la valeur d’un paramètre ou de comparer deux valeurs passées en paramètres. Si l’assertion est fausse, ces méthodes produisent une exception qui fait échouer le test.

Parmi les méthodes d’assertion, on trouve :

Méthode

Utilisation

assertTrue($condition)

Vérifie que la condition passée en paramètre est vraie.

assertFalse($condition)

Vérifie que la condition passée en paramètre est fausse.

assertEquals($expected, $actual)

Compare les deux paramètres pour vérifier qu’ils sont égaux.

assertNotEquals($expected, $actual)

Compare les deux paramètres pour vérifier qu’ils ne sont pas égaux.

assertSame($expected, $actual)

Vérifie que les deux objets passés en paramètre sont en fait le même objet en mémoire.

assertNotSame($expected, $actual)

Vérifie que les deux objets passés en paramètre ne sont pas les mêmes objets en mémoire.

assertNull($actual)

Vérifie que l’expression passée en paramètre s’évalue à null.

assertNotNull($actual)

Vérifie que l’expression passée en paramètre ne s’évalue pas à null.

Chacune des méthodes précédentes accepte une chaîne de caractères comme dernier paramètre optionnel. Il s’agit du message d’erreur produit dans le rapport de test si l’assertion échoue.

Note

Pour les méthodes d’assertion qui attendent deux valeurs pour les comparer, notez que la première valeur correspond à la valeur attendue pour ce test et la deuxième valeur correspond à la valeur produite au moment du test.

Exemple d’utilisation des assertions
<?php

use PHPUnit\Framework\TestCase;

class StringTest extends TestCase
{

    public function test_exemple_assertions()
    {
        $s = "Bonjour le monde";

        $this->assertEquals("Bonjour le monde", $s);
        $this->assertNotEquals("Bonsoir le monde", $s);
        $this->assertFalse(empty($s));
    }

}

Pour une présentation exhaustive des méthodes d’assertion, consultez la documentation officielle.

Exercice - Tests unitaires de abs

Écrire les tests unitaires pour la fonction abs.

Note

L’exercice précédent propose de tester une fonction sans effet de bord (ce que l’on appelle également une fonction pure). Les tests sur ce type de fonction sont faciles à écrire. Ils restent cependant l’exception lorsqu’on utilise la programmation orientée objet. En effet, l’appel d’une méthode sur un objet modifie le plus souvent son état et provoque généralement des effets de bord en sollicitant d’autres objets avec lesquels l’objet entretient des dépendances.

Les fixtures

Pour réaliser un test, il est parfois nécessaire de disposer d’un grand nombre d’objets correctement initialisés et de préparer le SUT (System Under Test). Plutôt que d’écrire le code nécessaire au début d’un test (au risque de le rendre moins lisible), on préfère écrire ce code dans une classe à part ou une méthode à part. Dans ce cas, on qualifie ce nouvel objet ou cette nouvelle méthode de fixtures.

Avec PHPUnit, il est possible d’exécuter des méthodes avant et après chaque test pour allouer et désallouer des ressources nécessaires à l’exécution des tests. On déclare pour cela des méthodes publiques sans paramètre annotées avec @before ou @after.

Exemple d’une classe de test avec @before et @after
<?php

use PHPUnit\Framework\TestCase;

class UneClasseTest extends TestCase
{

    /**
     * @before
     */
    public function initTestEnvironment()
    {
        // cette méthode est exécutée avant chaque test
    }

    /**
     * @after
     */
    public function destroyTestEnvironment()
    {
        // cette méthode est exécutée après chaque test
    }

    public function testMethode()
    {
        // la méthode de test
    }

}

Note

Il est également possible de déclarer des méthodes static annotées avec @beforeClass ou @afterClass. Ces méthodes ne sont appelées qu’une seule fois respectivement avant ou après l’ensemble des méthodes de test de la classe.

Utilisation de doublure

Parfois, il est utile de contrôler l’environnement de test d’un objet ou d’une collaboration d’objets. Pour cela, on peut faire appel à des doublures qui vont se substituer lors des tests aux objets réellement utilisés lors de l’exécution de l’application dans un environnement de production.

Simulateur

Un simulateur fournit une implémentation alternative d’un sous-système. Un simulateur remplace un sous-système qui n’est pas disponible pour l’environnement de test. Par exemple, on peut remplacer un système de base de données par une implémentation simplifiée en mémoire.

Fake object

Un fake object permet de remplacer un sous-système dont il est difficile de garantir le comportement. Le comportement du fake object est défini par le test et est donc déterministe. Par exemple, si un objet dépend des informations retournées par un service Web, il est souhaitable de remplacer pour les tests l’implémentation du client par une implémentation qui retournera une réponse déterminée par le test lui-même.

Mock object

Un objet mock est proche d’un fake object sauf qu’un objet mock est également capable de faire des assertions sur les méthodes qui sont appelées et les paramètres qui sont transmis à ces méthodes.

Implémentation d’objet mock

PHPUnit supporte la création d’objets mocks à partir d’une classe.

La méthode createMock($originalClassName) de la classe TestCase permet de créer une instance d’un mock à partir d’une classe. L’instance d’objet retournée par cette méthode est instrumentalisée. Il est possible d’enregistrer sur ce mock des comportements lors de la phase arrange grâce à la méthode expects($matcher) qui est disponible automatiquement sur l’objet mock. À la fin du test, PHPUnit vérifie automatiquement que l’objet mock a été sollicité comme attendu lors du déroulement du test.

Imaginons que notre application définisse une classe UtilisateurRepository responsable d’interagir avec une base de données. Par exemple, cette classe peut déclarer la méthode getUtilisateur. Pour garder notre exemple simple, nous ne fournirons que la structure de base des classes :

<?php

class Utilisateur
{
    // ...
}

class UtilisateurRepository
{
    /**
     *
     * @param int $idUtilisateur identifiant de l'utilisateur
     * @return Utilisateur l'utilisateur portant l'id demandé
     */
    public function getUtilisateur($idUtilisateur)
    {
        // ...
    }
}

Si un test unitaire a besoin de manipuler une instance de UtilisateurRepository mais qu’il ne souhaite pas exécuter le code réel et accéder à la base de données, il peut créer un objet mock et contrôler son comportement lors du test.

Exemple d’une classe de test utilisant un objet mock
<?php

use PHPUnit\Framework\TestCase;

class ExempleMockTest extends TestCase
{
    public function test_utilisateur_repository_mock()
    {
        $utilisateurRepository = $this->createMock(UtilisateurRepository::class);
        $utilisateur = new Utilisateur;

        // On programme le mock pour s'assurer que la méthode getUtilisateur
        // sera bien appelée avec le paramètre 1 et retournera l'objet $utilisateur.
        $utilisateurRepository->expects($this->once())
                              ->method("getUtilisateur")
                              ->with(1)
                              ->willReturn($utilisateur);

        $resultat = $utilisateurRepository->getUtilisateur(1);

        $this->assertSame($utilisateur, $resultat);
    }

}

L’exemple de code ci-dessus ne correspond bien évidemment pas à un vrai test unitaire. Les objets mock ne sont vraiment utiles que lorsqu’ils sont passés en paramètres de méthode ou de constructeur des objets à tester. Un objet mock sert avant tout à contrôler le comportement des objets dont dépend le SUT (System Under Test) et à vérifier qu’ils sont sollicités comme attendu.