PHPUnit
Vérifier automatiquement que son code fonctionne, sans avoir à tout retester à la main
Notions théoriques
Qu'est-ce que PHPUnit ?
PHPUnit est le framework de tests automatisés le plus utilisé pour le langage PHP. Il permet d'écrire du code qui vérifie que votre code fonctionne comme prévu, sans avoir besoin de le tester manuellement à chaque modification.
Pourquoi tester son code automatiquement ?
- Détecter les régressions : quand vous modifiez une classe, vous savez immédiatement si vous avez cassé un comportement qui fonctionnait avant.
- Documenter le comportement attendu : un test bien écrit montre clairement ce qu'une méthode est censée faire, pour n'importe quelle valeur entrée.
- Gagner du temps : plus besoin de relancer manuellement l'application et de vérifier à l'œil chaque scénario après chaque changement.
- Donner confiance avant de livrer une nouvelle version du programme.
Installation via Composer
PHPUnit s'installe comme dépendance de développement, avec Composer :
composer require --dev phpunit/phpunit
L'option --dev indique que PHPUnit n'est utile que pendant le développement (pour lancer les tests), pas en production. Il est ajouté dans la section require-dev du fichier composer.json.
Anatomie d'un test
Un test PHPUnit est écrit dans une classe qui étend PHPUnit\Framework\TestCase. Chaque méthode dont le nom commence par test est exécutée automatiquement comme un test indépendant.
<?php
use PHPUnit\Framework\TestCase;
class CalculatriceTest extends TestCase {
public function testAdditionne() {
$resultat = 2 + 2;
$this->assertEquals(4, $resultat);
}
}
Ici,
testAdditionne()est un test : il vérifie que2 + 2vaut bien4, grâce à une assertion.
Les assertions courantes
Une assertion vérifie qu'une condition est vraie ; si ce n'est pas le cas, le test échoue et PHPUnit affiche précisément où et pourquoi.
| Assertion | Vérifie que... |
|---|---|
assertEquals($attendu, $obtenu) | $attendu et $obtenu sont égaux (valeur) |
assertSame($attendu, $obtenu) | $attendu et $obtenu sont strictement identiques (valeur et type) |
assertTrue($condition) | $condition vaut true |
assertFalse($condition) | $condition vaut false |
assertNull($valeur) | $valeur vaut null |
assertCount($nombre, $tableau) | $tableau contient exactement $nombre éléments |
Exécuter les tests
Une fois PHPUnit installé, on lance tous les tests du projet avec :
vendor/bin/phpunit tests
PHPUnit affiche un point . pour chaque test réussi, un F pour chaque test en échec, puis un résumé.
Exemple de mise en application
Reprenons notre jeu de combat. Le projet TP2_phpunit contient déjà une classe Character et une classe Fight, organisées avec Composer et l'autoload PSR-4 :
{
"name": "bouquet/tp-tests-unitaires",
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "10"
}
}
<?php
namespace App;
class Character {
public $name;
public $health;
public $strength;
public function __construct($name, $health, $strength) {
$this->name = $name;
$this->health = $health;
$this->strength = $strength;
}
}
<?php
namespace App;
class Fight {
public $character1;
public $character2;
public function __construct(Character $character1, Character $character2) {
$this->character1 = $character1;
$this->character2 = $character2;
}
public function start() {
if ($this->character1->strength > $this->character2->strength) {
return $this->character1;
} else if ($this->character1->strength < $this->character2->strength) {
return $this->character2;
} else {
return null; // Il n'y a pas de vainqueur, donc on renvoie null
}
}
}
Le fichier tests/FightTest.php vérifie deux comportements : que le combat retient bien les deux personnages passés au constructeur, et que le personnage le plus fort remporte le combat :
<?php
use App\Fight;
use App\Character;
use PHPUnit\Framework\TestCase;
class FightTest extends TestCase {
public function testConstructor() {
$character1 = new Character('Guerrier', 100, 20);
$character2 = new Character('Monstre', 80, 15);
$fight = new Fight($character1, $character2);
$this->assertSame($character1, $fight->character1);
$this->assertSame($character2, $fight->character2);
}
public function testStart() {
$character1 = new Character("John", 100, 50);
$character2 = new Character("Jane", 100, 60);
$fight = new Fight($character1, $character2);
$winner = $fight->start();
$this->assertSame($character2, $winner);
}
}
En lançant vendor/bin/phpunit tests, ces deux tests s'exécutent et confirment que Fight se comporte comme prévu, sans qu'il soit nécessaire d'écrire une seule ligne de code pour les tester manuellement.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Le projet TP2_phpunit gère un combat entre deux personnages, mais aucune attaque ne fait réellement perdre de points de vie. Nous allons ajouter une méthode attack() à la classe Character, puis écrire des tests PHPUnit pour vérifier son comportement.
Étape 1 — Installer PHPUnit dans le projet
Dans le dossier TP2_phpunit, installez PHPUnit comme dépendance de développement.
Toujours installer PHPUnit avec --dev : cela évite d'alourdir inutilement le déploiement en production avec des outils qui ne servent qu'aux développeurs.
Étape 2 — Ajouter la méthode attack() à Character
Ajoutez à la classe Character une méthode attack(Character $target) qui réduit les points de vie (health) de la cible $target de la valeur de la force (strength) de l'attaquant. Les points de vie ne doivent jamais descendre en dessous de 0.
Vérifiez toujours les valeurs limites (ici, ne jamais descendre sous 0 point de vie). Sans cette protection, un personnage pourrait avoir des points de vie négatifs, ce qui n'a pas de sens dans le jeu et compliquerait la détection de la défaite.
Étape 3 — Tester qu'une attaque réduit les points de vie
Ajoutez dans tests/FightTest.php un test testAttackReducesHealth qui vérifie qu'après une attaque, les points de vie de la cible ont bien diminué du montant attendu.
Un test doit vérifier un seul comportement précis (ici, la perte de points de vie), avec des valeurs simples à recalculer de tête. Cela rend l'échec du test immédiatement compréhensible, sans avoir à déchiffrer un scénario complexe.
Étape 4 — Tester la défaite à 0 point de vie
Ajoutez un second test testCharacterDefeatedWhenHealthReachesZero qui vérifie qu'un personnage attaqué avec une force supérieure à ses points de vie restants tombe bien exactement à 0 (et jamais en dessous).
Ne testez pas uniquement le cas "normal" (une attaque qui réduit sans achever). Testez aussi le cas limite où l'attaque devrait faire passer les points de vie sous 0 : c'est souvent à ces frontières que les bugs se cachent.