Référence : https://phpunit.readthedocs.io/fr/latest/installation.html

Définition

Source : wikipedia

En programmation informatique, le test unitaire (ou « T.U. », ou « U.T. » en anglais) est une procédure permettant de vérifier le bon fonctionnement d'une partie précise d'un logiciel ou d'une portion d'un programme (appelée « unité » ou « module »).

A quoi ça sert ?

Martin Fowler : Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead.

Lors d'une modification d'un programme, les tests unitaires signalent les éventuelles régressions. En effet, certains tests peuvent échouer à la suite d'une modification, il faut donc soit réécrire le test pour le faire correspondre aux nouvelles attentes, soit corriger l'erreur se situant dans le code.

La méthode XP préconise d'écrire les tests en même temps, ou même avant la fonction à tester (Test Driven Development). Ceci permet de définir précisément l'interface du module à développer. Les tests sont exécutés durant tout le développement, permettant de visualiser si le code fraîchement écrit correspond au besoin.

On écrit un test pour confronter une réalisation à sa spécification. Le test définit un critère d'arrêt (état ou sorties à l'issue de l'exécution) et permet de statuer sur le succès ou sur l'échec d'une vérification. Grâce à la spécification, on est en mesure de faire correspondre un état d'entrée donné à un résultat ou à une sortie. Le test permet de vérifier que la relation d'entrée / sortie donnée par la spécification est bel et bien réalisée.

 

Fonctionnement

On définit généralement 4 phases dans l'exécution d'un test unitaire :

  1. Initialisation (fonction setUp) : définition d'un environnement de test complètement reproductible (une fixture).
  2. Exercice : le module à tester est exécuté.
  3. Vérification (utilisation de fonctions assert) : comparaison des résultats obtenus avec un vecteur de résultat défini. Ces tests définissent le résultat du test : SUCCÈS (SUCCESS) ou ÉCHEC (FAILURE). On peut également définir d'autres résultats comme ÉVITÉ (SKIPPED).
  4. Désactivation (fonction tearDown) : désinstallation des fixtures pour retrouver l'état initial du système, dans le but de ne pas polluer les tests suivants. Tous les tests doivent être indépendants et reproductibles unitairement (quand exécutés seuls).

Installation de phpunit avec composer

Je choisis d'installer phpunit avec composer via la commande composer init. Vous verrez plus bas que je réponds "yes" à la question "Would you like to define your dev dependencies (require-dev) interactively" et c'est à ce niveau que je vais chercher le package phpunit (index 0).

Vous remarquerez que j'en profite pour :

  • installer un autoload "psr-4"
  • créer le namespace Coopernet\Testphpunit qui va être lié au répertoires "/src"
composer init
Package name (<vendor>/<name>) [yvan/test]: coopernet/testphpunit
Description []:
Author [Yvan Douënel <y.douenel@coopernet.fr>, n to skip]:
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []:
License []: 

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]? yes
Search for a package: phpunit
Info from https://repo.packagist.org:

Found 15 packages matching phpunit

   [0] phpunit/phpunit 
   [1] phpunit/php-timer 
   [2] phpunit/php-text-template 
   [3] phpunit/php-file-iterator 
   ...
Enter package # to add, or the complete package name if it is not listed: 0
Enter the version constraint to require (or leave blank to use the latest version):
Using version ^9.5 for phpunit/phpunit
Search for a package:
Add PSR-4 autoload mapping? Maps namespace "Coopernet\Testphpunit" to the entered relative path. [src/, n to skip]: 
{
    "name": "coopernet/testphpunit",
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "autoload": {
        "psr-4": {
            "Coopernet\\Testphpunit\\": "src/"
        }
    },
    "authors": [
        {
            "name": "Yvan Douënel",
            "email": "y.douenel@coopernet.fr"
        }
    ],
    "require": {}
}

Do you confirm generation [yes]?
Would you like to install dependencies now [yes]? 
Loading composer repositories with package information
Updating dependencies
Lock file operations: 34 installs, 0 updates, 0 removals
  - Locking doctrine/instantiator (1.4.1)
  - Locking myclabs/deep-copy (1.11.0)
  - Locking nikic/php-parser (v4.13.2)
  - Locking phar-io/manifest (2.0.3)
  - ...

Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 34 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-ctype (v1.25.0): Extracting archive
  - Installing webmozart/assert (1.10.0): Extracting archive
  - Installing phpdocumentor/reflection-common (2.2.0): Extracting archive
  - Installing phpdocumentor/type-resolver (1.6.1): Extracting archive
  - Installing phpdocumentor/reflection-docblock (5.3.0): Extracting archive
  - ...

5 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
26 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
PSR-4 autoloading configured. Use "namespace Coopernet\Testphpunit;" in src/
Include the Composer autoloader with: require 'vendor/autoload.php';

 

Premiers tests avec assertSame

Par convention, on crée les tests dans le répertoire tests à la racine de notre projet.

assertSame

Lors d'un test, on va utiliser des "assertions" (proposition que l'on avance et que l'on soutient comme vraie) qui seront validées ou invalidées.

La première assertion que l'on va utiliser ici est assertSame. Comme son nom l'indique, elle va vérifier si deux valeurs sont de même type et si elles ont la même valeur.

Tests d'opérations de tableaux - cf https://phpunit.readthedocs.io/fr/latest/writing-tests-for-phpunit.html

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase {

  public function testPushAndPop() {
    $stack = [];
    $this->assertSame(0, count($stack));

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

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

}

Lancer le test sans puis avec options :

vendor/bin/phpunit  tests/StackTest.php
vendor/bin/phpunit tests/StackTest.php --color
vendor/bin/phpunit tests/StackTest.php --color --testdox

Dépendances entre tests

Référence : https://phpunit.readthedocs.io/fr/latest/writing-tests-for-phpunit.html

L'annotation @depends permet d'exprimer des dépendances entre des méthodes de test.

Producteur : Un producteur est une méthode de test qui produit ses éléments testées comme valeur de retour. En d'autres termes, un "producteur" va passer à un "consommateur" une valeur dont on sait qu'elle a certaines propriétés qui sont nécessaire pour le test "consommateur". Par exemple pour tester la suppression d'un élément de tableau, encore faut-il s'assurer que ce tableau n'est pas vide.

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

Exemple :

<?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->assertSame('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

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

}

Fixture

Référence : https://phpunit.readthedocs.io/fr/latest/fixtures.html

L’une des parties les plus consommatrices en temps lors de l’écriture de tests est d’écrire le code pour configurer le monde dans un état connu puis de le remettre dans son état initial quand le test est terminé. Cet état connu est appelé la fixture du test.

Dans le premier code de cette page (testPushAndPop) la fixture était simplement le tableau sauvegardé dans la variable $stack. La plupart du temps, cependant, la fixture sera beaucoup plus complexe qu’un simple tableau, et le volume de code nécessaire pour la mettre en place croîtra dans les mêmes proportions. 

PHPUnit gère le partage du code de configuration. Avant qu’une méthode de test ne soit lancée, une méthode template appelée setUp() est invoquée. setUp() est l’endroit où vous créez les objets sur lesquels vous allez passer les tests. Une fois que la méthode de test est finie, qu’elle ait réussi ou échoué, une autre méthode template appelée tearDown() est invoquée. tearDown() est l’endroit où vous nettoyez les objets sur lesquels vous avez passé les tests.

Exemple d'utilisation de la méthode setUp

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = [];
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush()
    {
        array_push($this->stack, 'foo');
        $this->assertSame('foo', $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop()
    {
        array_push($this->stack, 'foo');
        $this->assertSame('foo', array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
 
}

Vous remarquerez que bien que la méthode testPush ait été appelée avant la méthode testPop, $this->stack reste un tableau vierge. C'est l'effet de tearDown qui est implicitement appelée.

Fournisseur de données

Une méthode de test peut recevoir des arguments arbitraires. Ces arguments doivent être fournis par une ou plusieurs méthodes fournisseuses de données

La méthode fournisseuse de données à utiliser est indiquée dans l’annotation @dataProvider.

Exemple de test avec founisseur de données 

<?php
use PHPUnit\Framework\TestCase;

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

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}

Doublure de test avec createMock

Il peut parfois être nécessaire de remplacer des classes existantes par une doublure (appel d'un endpoint par exemple).

On peut alors utiliser la méthode createMock.

La classe à "bouchonner" (stub)

<?php
class SomeClass
{
    public function doSomething()
    {
        // Do something.
    }
}

L'utilisation de createMock

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub()
    {
        // Créer un bouchon pour la classe SomeClass.
        $stub = $this->createMock(SomeClass::class);

        // Configurer le bouchon.
        $stub->method('doSomething')
             ->willReturn('foo');

        // Appeler $stub->doSomething() va maintenant retourner
        // 'foo'.
        $this->assertSame('foo', $stub->doSomething());
    }
}

Configuration avec phpunit.xml

Référence : https://phpunit.readthedocs.io/fr/latest/configuration.html

Ajouter le fichier phpunit.xml suivant à la racine de votre package :

<?xml version="1.0" encoding="utf-8" ?>
<phpunit  colors="true" verbose="true">
    <testsuites>
        <testsuite name="My Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

Constatez que vous pouvez lancer votre test simplement en entrant dans la console :

vendor/bin/phpunit

A noter : si vous souhaitez opérer des tests en appelant les fichiers dans un ordre précis, il vous suffit d'utiliser la balise <file> à la place de <directory> et d'appeler les fichiers dans l'ordre choisi

Test Driven Development (TDD)

Le développement piloté par les tests est une méthode de développement qui suit le cycle suivant :

  • cycle de testvous créez un test
  • vous mettez ce test en échec
  • vous changez le code afin que le test passe
  • vous refactorisez le code si nécessaire

 

Katas

Un kata de code est un exercice de programmation qui permet aux programmeurs de perfectionner leurs compétences à travers la pratique et la répétition. 

kata

Pour créer des tests qui ont du sens, il nous faut une classe avec une ou plusieurs méthodes qui font un calcul pas complètement trivial. Cela nous permettra de comprendre comment mettre en place une classe de test afin vérifier que le résultat est juste dans différents cas de figure.

Kata "Fizz Buzz"

L’objectif de ce kata est de faire une boucle de 1 à 100 (par exemple). Pour chaque entier multiple de 3, il faut afficher la chaîne de caractères  "Fizz", si l'entier est un multiple de 5 on affiche la chaîne de caractères "Buzz". Si l'entier est un multiple de 3 et 5 il faut afficher la chaîne de caractères  "FizzBuzz" sinon on affiche le nombre lui même.

Au final cela va donner le programme devra afficher : 1, 2, "Fizz", 4, "Buzz", 6, ...

Dans un TDD, on réfléchit d'abord en terme de test et on écrit le code à tester ensuite.

Il faut toujours commencer par un test simple d'échec que l'on passe ensuite en succès.

Ici le test pourrait s'appeler "testOneForOne() " et s'écrirait  dans le répertoire tests de la manière suivante :

<?php

use PHPUnit\Framework\TestCase;

class FizzBuzzTest extends TestCase {

  protected FizzBuzz $fizzBuzz;

  protected function setUp(): void {
    $this->fizzBuzz = new FizzBuzz();
  }

  public function testOneForOne() {
    $this->assertEquals(1, $this->fizzBuzz->getFizzBuzz(1));
  }
}

On comprend donc que c'est d'abord le test qui nous pousse à écrire nos développements. En l'occurence, il nous faut maintenant écrire la classe FizzBuzz dans src :

<?php

class FizzBuzz {

  public function getFizzBuzz($number) {
    
    return 0;
  }

}

Après avoir vérifiéque le test est en échec, ré-écrivons la méthode de façon à ce que le test soit un succès :

<?php

class FizzBuzz {

  public function getFizzBuzz($number) {
    
    return $number;
  }

}

Retournons aux tests, on comprend que la méthode de test suivante pourrait être :

public function testTwoForTwo() {
    $this->assertEquals(2, $this->fizzBuzz->getFizzBuzz(2));
}

... qui sera une réussite.

Passons au test suivant : 

public function testFizzForThree() {
    $this->assertEquals("Fizz", $this->fizzBuzz->getFizzBuzz(3));
}

qui va nous obliger à améliorer notre méthode fizzBuzz (c'est ça le "refactoring") :

public function getFizzBuzz($number) {
    if ($number === 3) {
      return "Fizz";
    } 
    return $number;
}

... soyons fous et rendons cela plus générique :

public function getFizzBuzz($number) {
    if ($number % 3 === 0) {
      return "Fizz";
    } 
    return $number;
}

En continuant dans la même logique, il nous reste à écrire les fonctions testBuzzForFive et testFizzBuzzForFifteen en modifiant la méthode getFizzBuzz en conséquence.

Je vous laisse faire !

Kata année bissextile

Une année bissextile (ou un an bissextil) est une année comportant 366 jours au lieu des 365 pour une année commune. Le jour ajouté est le 29 février car ce mois compte habituellement vingt-huit jours dans le calendrier grégorien. Les années sont en général bissextiles si elles sont multiples de quatre, toutefois elles ne le sont pas si elles sont multiples de cent à l'exception des années multiples de quatre cents qui sont elles bissextiles. C'est ainsi que les années 2020, 2024 et 2028 sont bissextiles, et que 2000 et 2400 le sont aussi, mais pas 1900, 2100, 2200 et 2300.

Exercice : Créez une classe LeapYear qui a une méthode statique isLeapYear qui attend en paramètre un nombre entier et qui renvoie true ou false si le nombre correspond à une année bissextile ou pas.

Kata nombre romains

Votre mission, si vous l'acceptez est de coder un convertisseur de nombres arabes vers les nombres romains.

Pour vous aider, voici les explications et des convertisseurs en ligne

Il vous faudra coder une classe de test : 

class ArabicToRomanTest extends TestCase {

  public function test1() {
    $this->assertEquals("I", ArabicToRoman::convertToRoman(1));
  }
}

... et la classe ArabicToRoman dont voici la base

<?php
class ArabicToRoman {
  public static function convertToRoman($arabic) {
    $roman = "";

    return $roman;
  }
}

Kata  "Bowling"

Le kata bowling consite à calculer le score d'une partie de bowling, ce qui n'est pas si évident. Il faut vérifier que le calcul est bon avec ou sans strike & spare, s'il y a plusieurs strike à la suite, etc.
Vous trouverez ici les règles de calculs du score au bowling.

Création de la classe Bowling

A partir de ces règles, créez dans le répertoire src une classe Bowling qui a pour proprété :

  • $throws (un tableau dans lequel on stockera le résultat de chaque lancé de boule)

avec les méthodes :

  • d'affectation d'un nombre de quilles tombées (de 0 à 10) lors d'un lancé (méthode throwBall($pins_number))
  • de calcul global du score (calculateFinalScore). Cette méthode parcourra les frames (de 0 à 9) et testera successivement le cas où le lancer a donné un strike (isStrike) ou un spare (isSpare) ou par défaut deux lancers dont la somme sera fatalement inférieur à 10. Le but est d'incrémenter correctement la variable $score pour finir par la retourner.
  • de calcul du bonus en cas de spare (bonusSpare)
  • de calcul de bonus en cas strike (bonusStrike)

Je vous laisse une heure pour créer cette classe. Entraidez-vous.

Création de la classe BowlingTest qui hérite de TestCase

Pour tester la classe Bowling, créez la classe BowlingTest

qui a pour propriété :

  • $game vierge au départ mais que l'on va assigner dans setUp ( $this->game = new Bowling();)

avec les méthodes :

  • throwMany($number, $pins) (permet de lancer plusieurs boules ($number) avec le même nombre de quilles couchées)
  • throwSpare() qui exécute deux lancé dont le total à 10. Par exemple :
      $this->game->throwBall(9);
      $this->game->throwBall(1);
  • throwStrike qui exécute un lancé de 10 : this->game->throwBall(10);
  • et enfin une série de fonction de test pour tester au moins une dizaine de cas :
    • aucune quille touchée pour les 20 lancés
    • 12 strikes
    • des lancés avec quelques strikes
    • des lancés avec quelques spares
    • ...