Prérequis
- Avoir installé php
- Avoir installé composer
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 :
- Initialisation (fonction setUp) : définition d'un environnement de test complètement reproductible (une fixture).
- Exercice : le module à tester est exécuté.
- 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).
- 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 StackTestSetUp extends TestCase { protected $stack; protected function setUp(): void { $this->stack = []; } protected function tearDown(): void { $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) qui se trouve dans le répertoire src
<?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" testdox="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 :
vous 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.

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", "Fizz", 7, 8 , "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz", ...
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; } }
C'est dur, besoin d'un indice ?
Pensez que tant que le nombre est supérieur à 10 (par exemple), il faut ajouter X au chiffre romain et soustraire 10 au nombre arabe.
Par ailleurs, la solution passe par des tableaux de conversion du type :
const ARABIC_DIGITS = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; const ROMAN_DIGITS = ["M","CM", "D","CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
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
- ...
Pas facile ... besoin d'un indice ?