• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 18/03/2019

Premiers pas avec PHPUnit et les tests unitaires

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Au chapitre précédent, vous avez installé la librairie PHPUnit pour tester votre code PHP. Dans ce chapitre, vous allez vous lancer dans le développement de tests unitaires. Première étape : comprendre ce qu'est un test unitaire.

Qu'est-ce qu'un test unitaire ?

Le principe d'un test unitaire est très simple : il s'agit d'exécuter du code provenant de l'application à tester et de vérifier que tout s'est bien déroulé comme prévu.

Cela signifie en général que vous allez vérifier que la valeur retournée est bien la valeur attendue et/ou que certaines méthodes ont bien été appelées.

Ce qu'il y a à tester
Ce qu'il y a à tester

Un test unitaire est dit "test boîte blanche". À la différence d'un "test boîte noire", il faut connaître le code (l'avoir lu au moins une fois) pour être en mesure de le tester. Et c'est logique car, lors de l'écriture d'un test unitaire, vous allez devoir appeler le code explicitement.

Premiers tests unitaires

Reprenons le projet Symfony mis en place au chapitre précédent, avec PHPUnit installé.

Supprimez le dossier AppBundle contenu dans le dossier tests de votre application Symfony, nous n'en avons pas besoin pour cette application.

Nous allons écrire du code contenant de la logique métier pour pouvoir la tester unitairement ensuite. Dans notre projet, créez une classe Product, ainsi qu'une méthode en charge du calcul de la TVA :

<?php

namespace AppBundle\Entity;

class Product
{
    const FOOD_PRODUCT = 'food';

    private $name;

    private $type;

    private $price;

    public function __construct($name, $type, $price)
    {
        $this->name = $name;
        $this->type = $type;
        $this->price = $price;
    }

    public function computeTVA()
    {
        if (self::FOOD_PRODUCT == $this->type) {
            return $this->price * 0.055;
        }

        return $this->price * 0.196;
    }
}

Maintenant, vous allez tester cette méthode computeTVA.

Cette méthode n'a pas de point d'entrée (argument de méthode). En revanche, il nous faut un produit ayant un type et un prix pour être en mesure de calculer la taxe à valeur ajoutée (TVA).

Combien de test(s) va-t-il falloir implémenter ?

Une indication pour connaître le nombre de tests qu'il va falloir écrire concerne les sorties possibles de la méthode. Dans le code présenté ci-dessus, vous voyez deux return :  il va falloir deux tests pour faire en sorte que tous les cas soient couverts.

Créer la classe de test

Tous les tests doivent être écrits dans des classes. Vous allez donc créer votre première classe de tests. Comme vous écrivez ces tests dans un projet Symfony, il y a quelques règles à respecter :

  • Cette classe doit être contenue dans le dossier tests du projet.

  • Il faut reproduire l'arborescence de la classe que vous souhaitez tester.

  • La classe de test doit avoir le même nom que la classe à tester, suffixée par Test.

La classe à créer est donc la suivante, dans le dossier tests/AppBundle/Entity :

<?php

namespace Tests\AppBundle\Entity;

class ProductTest
{
}

Vous allez faire appel à des méthodes de la librairie PHPUnit. Pour ce faire, il vous faut étendre la classe TestCase :

<?php

namespace Tests\AppBundle\Entity;

use PHPUnit\Framework\TestCase;

class ProductTest extends TestCase
{

}

Ça y est ! Vous êtes prêts à écrire votre premier test.

Créer les méthodes de test

Une règle à respecter : le nom de toutes vos méthodes de test doit être préfixé par test.

Test n°1

Le premier test doit couvrir le cas permettant de faire en sorte d'atteindre la ligne 25 de la classe Product lors de l'exécution de code de la méthode computeTVA.  Il faut donc que le produit ait un type "food". Allons-y ! ^^

<?php

namespace Tests\AppBundle\Entity;

use AppBundle\Entity\Product;
use PHPUnit\Framework\TestCase;

class ProductTest extends TestCase
{
    public function testcomputeTVAFoodProduct()
    {
        $product = new Product('Un produit', Product::FOOD_PRODUCT, 20);

        $this->assertSame(1.1, $product->computeTVA());
    }
}

Dans un premier temps, il s'agit de construire l'objet dont nous allons avoir besoin pour appeler la méthode computeTVA, puis de faire appel à PHPUnit pour effectuer une assertion (ligne 14).

Qu'est-ce qu'une assertion ?

Concrètement, du fait que nous étendons la classe PHPUnit\Framework\TestCase, nous avons la possibilité d'appeler la méthode assertSame qui prend en paramètre deux valeurs : la valeur attendue et le résultat du code exécuté. Le résultat devrait être 1.1 (puisque 20*0.055 = 1.1).

Il ne vous reste plus qu'à lancer la suite de tests avec la commande suivante :

$ vendor/bin/phpunit

Et voici le résultat que vous devriez obtenir :

Lancement des tests PHPUnit
Lancement des tests PHPUnit

Super ! Tout s'est bien passé ! :D

Lire et comprendre les résultats de tests

Nous avons quelques informations communiquées :

  • le point (.) correspond à une méthode de test (un seul dans notre cas) ;

  • le temps et la mémoire utilisée lors du lancement des tests ;

  • OK correspond au statut global des tests de l'application. Dans le cas où au moins l'un des tests échoue, PHP affichera la sortie suivante :

    Sortie si un test ne passe pas
    Sortie si un test échoue

Test n°2

Il nous reste encore un chemin dans le code à tester : le cas dans lequel un produit a un type différent de "food". Avant de regarder la correction, je vous invite à faire l'exercice seul, vous avez toutes les clés en mains !

Il vous suffit de créer une nouvelle méthode dans notre classe de test pour créer un produit ayant un type différent, puis d'appeler la méthode à tester et enfin d'écrire dans une assertion le résultat attendu :

<?php

namespace Tests\AppBundle\Entity;

use AppBundle\Entity\Product;
use PHPUnit\Framework\TestCase;

class ProductTest extends TestCase
{
    // …

    public function testComputeTVAOtherProduct()
    {
        $product = new Product('Un autre produit', 'Un autre type de produit', 20);

        $this->assertSame(3.92, $product->computeTVA());
    }
}

Lancez les tests de nouveau pour vous assurer que tout fonctionne comme prévu :

Exécution des tests unitaires couvrant tous les cas
Exécution des tests unitaires couvrant tous les cas

Et voilà ! Nous avons testé tous les cas présents dans le code fonctionnel. Notre code est maintenant bien plus robuste ! Nous sommes désormais certains que lors d'un changement dans le code original, le fonctionnement du code est assuré grâce à nos tests unitaires automatisés.

Quand l'un des chemins de code (ou cas) lève une exception

Nous venons de voir que le nombre de return dans une méthode est une des indications pour connaître le nombre de tests à créer. Cependant, ce n'est pas la seule manière de sortir d'une méthode ! Le code écrit peut, en fonction des cas, lever des exceptions.

Reprenez notre code : nous allons y ajouter ce qu'il faut pour faire en sorte que lorsqu'un prix est négatif, une exception soit lancée.

<?php

namespace AppBundle\Entity;

class Product
{
    // …

    public function computeTVA()
    {
        if ($this->price < 0) {
            throw new \LogicException('The TVA cannot be negative.');
        }

        //…
    }
}

Dans le cas où le prix du produit est inférieur à 0, une exception de type LogicException est lancée.

Il nous faut tester ce nouveau cas. Voici comment procéder :

<?php

namespace Tests\AppBundle\Entity;

use AppBundle\Entity\Product;
use PHPUnit\Framework\TestCase;

class ProductTest extends TestCase
{
    // …

    public function testNegativePriceComputeTVA()
    {
        $product = new Product('Un produit', Product::FOOD_PRODUCT, -20);

        $this->expectException('LogicException');

        $product->computeTVA();
    }
}

La façon de procéder est un peu différente de ce que je vous ai montré précédemment. Il s'agit d'indiquer ce qui est attendu avant d'exécuter le code à tester : la méthode expectException prend en paramètre une chaîne de caractères correspondant au FQCN (Full Qualified Class Name, c'est-à-dire le namespace complet de la classe) de l'exception qui devrait être levée au moment de l'exécution du code. Cette méthode nous permet d'ajouter une nouvelle assertion à notre suite de tests !

Résultat de la suite de tests
Résultat de la suite de tests

Créer une suite de tests avec une suite de valeurs définies : les data providers

Avec les tests unitaires automatisés, il est important de tenter de couvrir l'ensemble du code (les chemins), mais aussi les cas limites liés à la logique métier de l'application. Il est aussi intéressant de s'assurer que son code fonctionne avec une suite de valeurs en entrée différentes sans pour autant avoir à créer une méthode de test différente.

Pour répondre à cette problématique, il existe les data provider ("fournisseur de données" en français).

Modifions la méthode de test testcomputeTVAFoodProduct pour faire en sorte d'exécuter cette méthode de tests avec un jeu de données particulier :

<?php

namespace Tests\AppBundle\Entity;

use AppBundle\Entity\Product;
use PHPUnit\Framework\TestCase;

class ProductTest extends TestCase
{
    /**
     * @dataProvider pricesForFoodProduct
     */
    public function testcomputeTVAFoodProduct($price, $expectedTva)
    {
        $product = new Product('Un produit', Product::FOOD_PRODUCT, $price);

        $this->assertSame($expectedTva, $product->computeTVA());
    }

    public function pricesForFoodProduct()
    {
        return [
            [0, 0.0],
            [20, 1.1],
            [100, 5.5]
        ];
    }
    
    // …
}

Au moment où PHPUnit appelle la méthode testcomputeTVAFoodProduct lors du lancement des tests, celle-ci sera en réalité appelée trois fois de suite en passant les paramètres suivants, tour à tour :

  • $price = 0 et $expectedTva = 0.0 puis,

  • $price = 20 et $expectedTva = 1.1 et enfin,

  • $price = 100 et $expectedTva = 5.5.

Grâce à l'annotation @dataprovider, PHPUnit est en mesure de récupérer les données via la méthode indiquée dans l'annotation (pricesForFoodProduct). Cette dernière doit retourner un tableau de tableau(x) avec autant d'éléments que de paramètre(s) que l'on souhaite passer à la méthode de test qui recevra les données pour les exploiter.

Tapez la commande suivante et voyez le résultat :

Résultats test avec data provider
Résultats test avec data provider

Comme vous pouvez le voir, même si nous lançons les tests uniquement pour une méthode de test, celle-ci est bien exécutée 3 fois.

Configuration de PHPUnit

Il est possible de configurer la manière dont PHPUnit doit se comporter lors du lancement des tests. Dans un projet Symfony, le fichier responsable de la configuration est contenu à la racine du projet, dans le fichier phpunit.xml.dist. Libre à vous de le modifier pour en modifier les règles.

Pour en savoir plus sur comment modifier le fichier selon vos besoins, rendez-vous sur la documentation officielle de PHPUnit.

Et voilà ! Je vous félicite pour vos premiers tests unitaires ! ^^

Rendez-vous au prochain chapitre pour apprendre à tester unitairement du code qui est dépendant de systèmes externes.

Exemple de certificat de réussite
Exemple de certificat de réussite