• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 12/05/2022

Faites vos premiers pas avec PHPUnit et les tests unitaires

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. Mais avant, nous allons voir concrètement ce qu’est un test unitaire, et en quoi cela consiste.

Comprenez le principe d’un test unitaire

Le principe d'un test unitaire est très simple. Il s'agit :

  1. D'exécuter du code provenant de l'application à tester.

  2. 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

On dit qu’un test unitaire est un test “boîte blanche”. À la différence d'un test “boîte noire", il faut connaître le code un minimum pour être en mesure de le tester ; c’est-à-dire l’avoir lu au moins une fois. C'est logique car, lors de l'écriture d'un test unitaire, vous allez devoir appeler le code explicitement.

Faites vos premiers tests unitaires

Créez les méthodes de test

Continuons notre cours avec notre version de PHPUnit fonctionnelle…

L’application que nous cherchons à tester s’appelle Food-diary, et sert à noter ce que l’on mange tous les jours, un peu comme un journal. Nous n’allons pas tester directement cette partie, mais plutôt le pricing avec le calcul de TVA, avec pour but futur de pouvoir créer une liste de courses, par exemple.

Nous allons reprendre le code que vous avez récupéré juste avant et contenant de la logique métier, pour pouvoir la tester unitairement ensuite. Dans notre projet, voici la classe  Product  , ainsi qu'une méthode en charge du calcul de la TVA :

<?php
namespace App\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 la 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 sur la valeur ajoutée (TVA).

Combien de tests 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.

Nous allons réaliser 2 tests :

  1. Test n° 1 : lorsque le type vaut “food”.

  2. Test n° 2 : lorsque le type est différent de food.

En fonction du type de produit rencontré, la TVA est différente. Il est donc indispensable de s’assurer que ce mécanisme est opérationnel et fonctionne bien, et c’est ce à quoi servent ces tests.

Pour le premier, le prix appliquera une TVA à 5,5 %, et pour le deuxième, une TVA à 19,6 %.

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 App\Tests\Entity;
use App\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());
    }
}

Il s'agit de :

  1. Construire l'objet dont nous allons avoir besoin pour appeler la méthode  computeTVA  .

  2. Puis de faire appel à PHPUnit pour effectuer une assertion.

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ètres 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 :

$ 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é !

Nous avons quelques informations communiquées :

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

  • la durée du test 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 échoue
Sortie si un test échoue
Test n° 2

Il nous reste encore un chemin dans le code à tester : le cas où un produit possède 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 main !

  1. Créez une nouvelle méthode dans notre classe de test pour créer un produit ayant un type différent.

  2. Puis appelez la méthode à tester.

  3. Et enfin écrivez dans une assertion le résultat attendu :

<?php
namespace App\Tests\Entity;
use App\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 ! De plus, 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.

Gérez les exceptions dans vos tests

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.

Reprenons 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. Ajoutez ce bout de code au début de votre méthode computeTVA, comme ci-dessous.

<?php
namespace App\Entity;
class Product
{
    // …
    public function computeTVA()
    {
       if ($this->price < 0) {
                throw new Exception('The TVA cannot be negative.');
       }
       //…
    }
}

Dans le cas où le prix du produit est inférieur à 0, une exception de type  Exception  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('Exception');
       $product->computeTVA();
    }
}

La façon de procéder est un peu différente de ce que vous avez vu 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

Dès qu’une exception est relevée dans les tests, le test passe en erreur, c’est tout à fait normal.

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

Avec les tests unitaires automatisés, il est important d’essayer de couvrir :

  • l'ensemble du code (les chemins) ;

  • mais aussi les cas limites liés à la logique métier de l'application.

C’est 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 providers” (“fournisseurs de données”, en français).

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

<?php
namespace App\Tests\Entity;
use App\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 :

  1. $price = 0 et $expectedTva = 0.0  .

  2. Puis  $price = 20 et $expectedTva = 1.1  .

  3. 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 tableaux, avec autant d'éléments que de paramètres 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 :

vendor/bin/phpunit --filter=testcomputeTVAFoodProduct
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.

Modifiez les règles de comportement de PHPUnit

Il est possible de configurer la manière dont PHPUnit doit se comporter lors du lancement des tests.

La modification du fichier de configuration peut servir à afficher les couleurs lors des tests, choisir le dossier de tests par défaut, et bien d’autres encore. Dans ces cas précis, il est judicieux de modifier ce fichier pour ne pas avoir à tout écrire dans la commande, mais là encore, ce ne sera que ponctuel.

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.

Il est tout de même rare que vous ayez à le modifier.

Félicitations : vous avez réalisé vos premiers tests unitaires !

En résumé

  • Un test unitaire permet de tester un cas avec un ou plusieurs points d’entrée, et de s’assurer que la sortie est celle que l’on attend.

  • Il faut s’assurer de couvrir tous les chemins de code. Pour cela, un  return  ou le lancement d’une exception sont de bons indicateurs de tests à effectuer.

  • Il faut néanmoins faire attention à ne pas vouloir avoir un taux de couverture de code (code coverage) à 100 %, car cela n'empêchera pas certains bugs, et c’est très chronophage ; donc privilégier ce qui est sensible.

Dans le prochain chapitre, vous allez apprendre à tester de façon unitaire du code qui est dépendant de systèmes externes. C’est parti !

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