Introduction aux tests unitaires avec PHPUnit 3.1
Date de publication : 15/08/2007 , Date de mise à jour : 15/08/2007
Par
Jean-Pierre Grossglauser (Page personnelle)
Ce tutoriel fournit une introduction aux tests unitaires PHP avec
le framework de tests PHPUnit.
I. Introduction
I-A. Remerciements
I-B. À qui s'adresse ce tutoriel ?
I-C. Prérequis
I-D. A propos des tests unitaires dans PHP
I-E. Méthodologie
II. PHPUnit
II-A. Installation
II-B. Utilitaire de ligne de commande
II-C. Écriture des tests
II-C-1. Générateur de gabarit
II-D. Organisation des tests
II-E. Exécution des tests
II-E-1. Lancer une suite de tests
II-E-2. Indicateurs de résultat
II-F. Exemple récapitulatif
III. Conclusion
III-A. Références
I. Introduction
I-A. Remerciements
I-B. À qui s'adresse ce tutoriel ?
Pour aborder ce tutoriel dans les meilleures conditions, vous devez :
I-C. Prérequis
I-D. A propos des tests unitaires dans PHP
Le langage PHP est un langage de programmation flexible,
permissif et facile à appréhender. Les principes et contraintes
de conception généralement imposées dans les langages à vocation
industrielle (C/C++, Java, etc.) ne s'appliquent pas nécessairement
avec PHP.
Le programmeur est seul maître à bord, il choisit la précision
avec laquelle il souhaite intégrer un paradigme, que ce soit
impératif, fonctionnel ou orienté objet, tout comme il décide
de structurer ou non son développement par rapport à des
processus éprouvés. Cette grande liberté est à l'origine
du succès du langage, mais c'est également son plus grand
défaut.
Beaucoup de développeurs PHP qui n'ont pas de formation
spécifique en programmation ou qui sont peu expérimentés ne
perçoivent pas l'importance d'un processus de développement,
et de test en particulier.
L'abscence d'une forme structurée de tests engendre notamment les
problématiques suivantes :
Le code source n'est pas testé en profondeur :
cela a pour conséquence des aléas de « post-publication »,
plus ou moins critiques. Le plus souvent il s'agit
d'instabilités dans l'application ou des problèmes
de sécurité classiques.
Le code source n'est pas robuste : toute modification
du code source (refactorisation, ajout de fonctionnalités) est
susceptible d'engendrer des régressions.
Le code source n'est pas réutilisable, pas transmissible : si un autre
développeur doit vous assister ou reprendre votre travail,
il sera confronté d'une part à votre code source et
d'autre part à l'absence d'un protocole de test uniformisé.
Le code source n'est pas évolutif :
il va sans dire, plus votre application aura une structure
complexe et plus vous peinerez à déceler des erreurs et
problèmes de conception de manière empirique.
Vous serez contraint à moyen terme, de
reprogrammer entièrement votre application.
Tout ceci aura non seulement un impact sur la
fiabilité de votre programme (et vos prestations de service),
mais également sur le temps alloué pour le développement
et la maintenance du projet, avec les tracas que cela
implique pour vous et vos utilisateurs finaux.
Bien que les tests unitaires soient primordiaux, ils ne résoudront pas les problèmes
d'analyse et conception (au mieux, ils les mettront en évidence), c'est pourquoi
vous devez garder en tête que la qualité finale de votre produit dépend de votre méthodologie
de travail dans son ensemble.
I-E. Méthodologie
Cette brève introduction méthodologique fournit un processus et quelques conseils vous permettant d'utiliser les tests unitaires dans vos projets PHP. Elle
n'a pas pour but d'être exhaustive, si vous souhaitez avoir d'amples informations sur les sujets abordés, consultez les références en fin de chapitre.
Le processus qui vous est proposé ci-dessus fait
abstraction des contraintes liées à la gestion de projet
(cahier des charges, organisation interne, etc.) ainsi que de
la phase d'analyse. Il est basé sur le développement
piloté par les tests (TDD) et s'adresse
particulièrement aux développeurs PHP autonomes.
Si vous développez en équipe, la base restera sensiblement la même
mais vous devrez fournir des précisions au niveau
de l'organisation et du partage des tâches, notamment.
Le développement piloté par les tests (Test-Driven Development, ou TDD)
est une méthode de développement mettant les tests unitaires
au premier plan, on peut résumer son processus en cinq étapes :
- Ajouter un test,
- vérifier qu'il ne passe pas,
- implémenter la fonctionnalité,
- exécuter les tests unitaires - déboguer en cas d'échec,
- refactoriser le code source - exécuter à nouveau les tests unitaires, passer à l'objectif suivant...
En reprenant le processus ci-dessus, vous observerez que l'approche TDD débute dès la phase de conception. Dans l'idéal, la structure de votre application
devrait être modélisée dans un éditeur UML puis son squelette généré directement dans des fichiers PHP.
Plusieurs éditeurs UML libres fournissent un générateur de code PHP 5 (nativement ou sous forme de plug-in), notamment
ArgoUML (multi-plateforme, notation
UML 1.4) et
StarUML (pour Windows uniquement, notation
UML 2.0).
Parallèlement, vous pouvez modéliser des
diagrammes entité-relation (ERD) et générer les schémas
de vos bases de données à la volée.
Pour ce faire il existe notamment les logiciels
DBDesigner 4 (logiciel libre et optimisé pour MySQL),
MySQL Workbench (version BETA) ainsi que
Toad Data Modeler (version freeware ou commerciale).
Dès que le squelette de votre applicaiton sera défini,
vous pourrez en faire de même avec les tests unitaires,
grâce à PHPUnit et son générateur de gabarits.
Enfin en observant la partie implémentation du processus, vous
retrouvez les étapes du TDD en bonne et due forme. La refactorisation
a été marquée optionnelle car elle pourrait être effectuée
durant l'implémentation de la méthode, mais aussi
dans le cadre d'une maintenance générale du code source. La refactorisation peut être considérée comme un objectif de développement à part entière.
D'amples informations :
II. PHPUnit
PHPUnit est un framework de test unitaires open source.
Quelques qualités...
- Un syntaxe simple, facile à comprendre et à retenir.
- Un grand nombre de méthodes de tests.
- Organisation et exécution des test flexibles.
- Un utilitaire en ligne de commande complet.
Quelques fonctionnalités avancées
- Support des objets "mock" (simulateur d'objets)
- Analyse de la couverture de code (code coverage analyse).
- Support de Selenium RC (tests fonctionnels)
- Journalisation des tests aux format XML, JSON, TAP ou dans une base de données.
PHPUnit est pris en charge nativement dans les IDE suivant :
- NuSphere PHPEd.
- PHPEdit.
- Zend Studio.
- PHPEclipse (voir plugin
Eclipse SimpleTest ).
- Eclipse PDT (le support a été prévu).
II-A. Installation
Vous pouvez utiliser PEAR :
| Interpréteur de commandes |
pear channel-discover pear.phpunit.de && pear install phpunit/PHPUnit
|
 |
PHPUnit 3.1.7 est utilisé dans ce tutoriel.
|
II-B. Utilitaire de ligne de commande
L'utilitaire de ligne de commande phpunit est
le principal outil dédié à la configuration et à l'exécution
des tests unitaires.
 |
Cet utilitaire est optimisé pour les systèmes Unix/Linux, si vous souhaitez l'utiliser avec Windows, vous devez :
Définir les variables d'environnement PHP_CLI (chemin vers l'applicatif php.exe) et PHPUNIT_HOME (chemin vers le répertoire de PHPUnit);
Créer un fichier *.bat dénommé phpunit.bat et y ajouter l'instruction suivante : %PHP_CLI% %PHPUNIT_HOME%\PHPUnit\TextUI\Command.php %*;
Indiquer le chemin vers le fichier phpunit.bat dans votre variable d'environnement PATH (ex. SET PATH=%PATH%;C:\chemin\vers\executable\).
|
| Option |
Description |
| --log-graphviz |
Journalise l'exécution du test au format GraphViz |
| --log-json |
Journalise l'exécution du test au format JSON. |
| --log-tap |
Journaliser l'exécution du test au format TAP. |
| --log-xml |
Journalise l'exécution du test au format XML. |
| --coverage-xml |
Génère un rapport de couverture de code (nécessite l'extension PHP Xdebug) au format XML. |
| --report |
Génére un rapport de couverture de code (nécessite l'extension PHP Xdebug) au format HTML. |
| --test-db-dsn |
DSN (Paramètres de connexion) de la base de données de test (Ndlr : Cette option journalise le résultat des tests et éventuellement la couverture de code dans une base de données) |
| --test-db-log-rev |
Information de révision pour la journalisation dans une base de données |
| --test-db-log-info |
Informations additionnelles concernant la journalisation dans une base de données |
| --testdox-html |
Génére la documentation Agile au format HTML. |
| --testdox-text |
Génére la documentation Agile au format texte. |
| --testdox-text |
Génére un rapport de couverture de code (nécessite l'extension PHP Xdebug). |
| --filter |
Filtre quels tests doivent être lancés (nom ou expression rationnelle). |
| --loader |
Spécifie le chargeur de classe à utiliser. |
| --repeat |
Nombre de répétitions des tests |
| --tap |
Reporte la progression du test au format TAP |
| --testdox |
Reporte la progression du test au format TestDox |
| --no-syntax-check |
Désactive le contrôle syntaxique des fichiers sources. |
| --stop-on-failure |
Interrompt l'exécution du test à la première erreur ou échec. |
| --verbose |
Affiche toutes les informations. |
| --wait |
Une touche doit être pressée entre chaque test. |
| --skeleton |
Génére le gabarit d'une classe de test. |
| --help |
Affiche le récapitulatif des options. |
| --version |
Affiche la version de PHPUnit utilisée. |
| -d key[=value] |
Définit une directive du php.ini |
II-C. Écriture des tests
II-C-1. Générateur de gabarit
Le générateur de gabarit (générateur de template, ou encore générateur de squelette) crée
des gabarits de code source pour vos tests. Il s'agit de
modèles de conception qui vous permettent d'augmenter votre
productivité tout en diminuant considérablement les
risques d'erreurs liés à l'organisation et à l'exécution des tests.
| Interpréteur de commandes |
phpunit --skeleton MyClass
|
Le générateur prendra en compte les méthodes publiques
et éventuellement les annotations d'assertion (voir plus bas) qui
leur sont associées.
 |
PHPUnit ne permet pas de tester des méthodes privées
ou protégées. Vous devez donc changer explicitement la visibilité
de ces dernières pour qu'elles soient prises en compte.
|
| Exemple de génération |
<?php
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'MyClassTest::main');
}
require_once 'PHPUnit/Framework.php';
require_once 'MyClass.php';
20070811200100
class MyClassTest extends PHPUnit_Framework_TestCase {
@access
@static
public static function main() {
require_once 'PHPUnit/TextUI/TestRunner.php';
$suite = new PHPUnit_Framework_TestSuite('MyClassTest');
$result = PHPUnit_TextUI_TestRunner::run($suite);
}
@access
protected function setUp() {
}
@access
protected function tearDown() {
}
@todo
public function testMyMethod() {
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
}
if (PHPUnit_MAIN_METHOD == 'MyClassTest::main') {
MyClassTest::main();
}
?>
|
Le générateur fournit une classe de test comportant des
méthodes avec l'annotation @todo Implement
(« À implémenter ») et un appel à ::markTestIncomplete(),
qui permet de marquer une méthode ou une
partie de son implémentation comme « incomplète » tant
qu'elle n'a pas été précisément remplie par le programmeur.
Vous noterez également les méthodes protégées
::setUp() et ::tearDown(); elles permettent
de fournir une fixture, autrement dit un "contexte
d'exécution" commun à toutes les méthodes de test. Dans
la pratique, il peut s'agir d'une connexion à une base de
données, ou toute autre instruction à vocation collective.
La méthode ::setUp() se comporte en constructeur
et s'exécute avant la méthode de test, tandis
que ::tearDown() s'exécute après celle-ci, faisant
office de destructeur.
 |
Les autres structures et notamment la méthode statique ::main() sont
relatives à l'organisation et l'exécution des tests.
Elles sont expliquées plus en détails au chapitre suivant.
|
Il est possible d'intégrer des tests d'assertion directement
dans les méthodes qui seront générées, pour ce faire vous
devez annoter les méthodes de la classe concrète en
utilisant la syntaxe suivante :
| Structure |
Description |
| @assert |
Balise d'assertion |
| (argument1[,argument2, ...]) |
Paramètres effectifs de la méthode à
tester. Il doit y en avoir au moins un.
|
| operator |
Opérateur de comparaison
(sera remplacé par la méthode d'assertion qui lui est associé).;
|
| value |
La valeur qui sera comparée avec la
valeur de retour de la méthode. |
Exemple :
class MyClass extends PHPUnit_Framework_TestCase
{
12
public function myMethod($value)
{
return $value;
}
}
|
Méthode de test générée :
12
public function testMyMethod() {
$constraint = $this->greaterThan(2);
$object = new myClass;
$value = $object->myMethod(1);
$this->assertThat($value, $constraint);
}
|
Ces annotions se limitent aux assertions suivantes :
| Opérateur |
Assertion |
| == |
Equals (égal à) |
| != |
NotEquals (différent de) |
| === |
Same (strictement égal à) |
| !== |
NotSame (strictement différent de) |
| > |
greaterThan (plus grand que) |
| >= |
greatThanOrEqual (plus grand ou égal à) |
| < |
lessThan (plus petit que) |
| <= |
lessThanOrEqual (plus petit ou égal à) |
 |
Utilisez cette fonctionnalité avec précaution !
|
II-D. Organisation des tests
PHPUnit offre une solution particulièrement
flexible pour organiser vos tests, vous pouvez les exécuter indépendamment
les uns des autres, les grouper en suites, ou
rassembler les suites elles-mêmes afin d'exécuter
le tout en une seule fois.
En reprenant une partie de l'exemple 1.0, vous remarquerez
que le générateur de gabarit propose plusieurs éléments dédiés à
la composition de tests :
| L'en-tête |
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'MyClassTest::main');
}
|
Cette structure définit la première méthode qui sera
exécutée si le fichier est appelé directement. Si
la classe de test est incluse dans une suite, la définition
sera ignorée.
| Méthode statique ::main() |
public static function main() {
require_once 'PHPUnit/TextUI/TestRunner.php';
$suite = new PHPUnit_Framework_TestSuite('MyClassTest');
$result = PHPUnit_TextUI_TestRunner::run($suite);
}
|
Par défaut, la méthode statique ::main() fournit une implémentation
minimale permettant de lancer le test et éventuellement d'y inclure
d'autres éléments (cas de test, suites ou autre instructions).
| Appel |
if (PHPUnit_MAIN_METHOD == 'MyClassTest::main') {
MyClassTest::main();
}
?>
|
Finalement, la structure ci-dessus appellera la méthode
définie en en-tête (::main, dans le cas précis) si le fichier est exécuté directement.
Pour créer une suite de tests, vous devez instancier la
classe PHPUnit_Framework_TestSuite, puis ajouter les tests
ou suites de tests qui vous intéresse avec les méthodes
::addTest et (ou) ::addTestSuite, comme le montre
l'exemple ci-dessous :
| Méthode statique ::main() |
public static function main() {
require_once 'PHPUnit/TextUI/TestRunner.php';
$suite = new PHPUnit_Framework_TestSuite('MyClassTest');
$suite->addTest('MyOtherClassTest');
$suite->addTestSuite('MyTestSuite');
$result = PHPUnit_TextUI_TestRunner::run($suite);
}
|
 |
En pratique, les instructions relatives aux suites ne devraient pas figurer
dans ::main() mais dans une méthode statique nommée (par convention)
::suite(); comme dans l'exemple ci-dessous :
|
| Méthode statique ::suite() |
public static function main()
{
PHPUnit_TextUI_TestRunner::run(self::suite());
}
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('All Tests');
$suite->addTest(MyOtherClassTest::suite());
}
|
De cette manière, vous obtiendrez un code source mieux décomposé et
plus lisible.
II-E. Exécution des tests
Pour lancer un seul cas de test ou une suite :
| Lancement du cas de test MyClass |
|
II-E-1. Lancer une suite de tests
Pour lancer tous les tests unitaires séquentiellement, vous devez créer
une classe principale comme ci-dessous, en y indiquant
tous les cas de test et suites qui vous intéressent :
| Classe AllTests |
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once 'MyClass.php';
require_once 'MyClassTest.php';
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'AllTests::main');
}
class AllTests
{
public static function main()
{
PHPUnit_TextUI_TestRunner::run(self::suite());
}
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('All Tests');
$suite->addTest(MyClassTest::main());
}
}
if (PHPUnit_MAIN_METHOD == 'AllTests::main') {
AllTests::main();
}
?>
|
 |
Pour lancer cette classe, vous devrez utiliser directement PHP :
|
| Interpréteur de commandes - Exécution d'un cas de test |
|
II-E-2. Indicateurs de résultat
Un indicateur de résultat est fourni pour chaque méthode de test exécutée :
| Indicateur |
Description |
| . |
Le test passe. |
| F |
Le test a échoué (Failure). |
| E |
Le test a généré une erreur PHP. |
| S |
Le test est ignoré (Skipped). |
| I |
Le test est marqué comme incomplet (Incomplete). |
II-F. Exemple récapitulatif
Voici un exemple récapitulatif de test unitaire.
La classe MyFile a pour attributs un nom (fileName) et une extension (extension)
de fichier.
Le nom de fichier doit être une chaîne de caractères de
de 1 à 32 caractères alphanumériques (ASCII), trait d'union
et sous-tiret compris. Tous les autres caractères
sont proscrits.
L'extension de fichier doit être composée de 1 à 5 caractères
alphanumériques (ASCII).
Diagramme de classe UML de MyFile :
- Le squelette de la classe MyFile est généré à partir du diagramme de classe susmentionné.
- Le gabarit de cas de test MyFileTest est généré à partir du squelette de la classe MyFile.
Implémentation du cas de test MyFileTest :
| MyFileTest.php |
<?php
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'MyFileTest::main');
}
require_once 'PHPUnit/Framework.php';
require_once 'MyFile.php';
20070812192550
class MyFileTest extends PHPUnit_Framework_TestCase {
@access
@static
public static function main() {
require_once 'PHPUnit/TextUI/TestRunner.php';
$suite = new PHPUnit_Framework_TestSuite('MyFileTest');
$result = PHPUnit_TextUI_TestRunner::run($suite);
}
public function testSetFileName()
{
$file = new MyFile;
$this->assertTrue($file->setFileName('newfile'));
$this->assertTrue($file->setFileName('newfile1'));
$this->assertTrue($file->setFileName('new_file'));
$this->assertTrue($file->setFileName('new-file'));
$this->assertFalse($file->setFileName(null));
$this->assertFalse($file->setFileName('ThisFileNameIsVeryLongTooLongReallyTooLong'));
$this->assertFalse($file->setFileName('NewFileWith.ext'));
$this->assertFalse($file->setFileName('àfile'));
$this->assertFalse($file->setFileName(1));
}
public function testSetExtension()
{
$file = new MyFile('newFile');
$this->assertTrue($file->setExtension('txt'));
$this->assertFalse($file->setExtension(null));
$this->assertFalse($file->setExtension('abcdef'));
$this->assertFalse($file->setExtension('é'));
$this->assertFalse($file->setExtension(1));
}
public function testGetFileName()
{
$exceptedFileName = 'newFile';
$file = new MyFile($exceptedFileName);
$this->assertEquals($exceptedFileName, $file->getFileName());
}
public function testGetExtension()
{
$exceptedExtension = 'txt';
$file = new MyFile('newFile', $exceptedExtension);
$this->assertEquals($exceptedExtension, $file->getExtension());
}
}
if (PHPUnit_MAIN_METHOD == 'MyFileTest::main') {
MyFileTest::main();
}
?>
|
Implémentation de la classe MyFile
| MyFile.php |
<?php
class MyFile
{
protected $fileName;
protected $extension;
public function __construct($fileName = null, $extension = null)
{
if (!is_null($fileName)) {
$this->setFileName($fileName);
}
if (!is_null($extension)) {
$this->setExtension($extension);
}
}
132
@paramstring$fileName
@returnboolean
public function setFileName($fileName)
{
if (!is_string($fileName)) {
return false;
}
if (!preg_match('/^[a-z0-9-_]{1,32}$/i',$fileName)) {
return false;
}
$this->fileName = $fileName;
return true;
}
15
@paramstring$extension
@returnboolean
public function setExtension($extension)
{
if (!is_string($extension)) {
return false;
}
if (!preg_match('/^[a-z0-9]{1,5}$/i',$extension)) {
return false;
}
$this->extension = $extension;
return true;
}
@returnstring
public function getFilename()
{
return $this->fileName;
}
@returnstring
public function getExtension()
{
return $this->extension;
}
}
?>
|
Exécution du test unitaire :
| Interpréteur de commandes |
phpunit MyFileTest
PHPUnit 3.1.7 by Sebastian Bergmann.
....
Time: 0 seconds
OK (4 tests)
|
 |
PHPUnit fournit quelques exemples complets de test unitaires,
ils sont disponibles dans le répertoire PHPUnit/Samples.
|
III. Conclusion
PHPUnit est un framework de test simple et efficace, son éventail de fonctionnalités permet de créer rapidement des tests unitaires complets et adaptés aux applications PHP professionnelles.
III-A. Références
Ressources Developpez.com :


Copyright © 2007 Jean-Pierre Grossglauser. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.