Introduction aux tests unitaires avec PHPUnit 3.1

Jean-Pierre Grossglauser

Ce tutoriel fournit une introduction aux tests unitaires PHP avec le framework de tests PHPUnit.

- Titre : Introduction aux tests unitaires avec PHPUnit 3.1

- Auteur : Jean-Pierre Grossglauser

- Parution : 15 août 2007

- Mise à jour : 9 décembre 2019

- Licence : 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.

Sommaire

- I. Introduction

- I-A. Remerciements

- I-B. À qui s'adresse ce tutoriel ?

- I-C. Prérequis

- I-D. À 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 gabarits

- 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

Mes remerciements à Guillaume Rossolini pour ses critiques et sa relecture.

I-B - À qui s'adresse ce tutoriel ?

Pour aborder ce tutoriel dans les meilleures conditions, vous devez :

- maîtriser le paradigme orienté objet PHP 5.

I-C - Prérequis

- Bibliothèque PEAR (Guide d'installation de PEAR).

- Pouvoir exécuter PHP en ligne de commande.

I-D - À 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'absence 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 « postpublication », 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 finals.

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 :

1. Ajouter un test ;

2. Vérifier qu'il ne passe pas ;

3. Implémenter la fonctionnalité ;

4. Exécuter les tests unitaires - déboguer en cas d'échec ;

5. 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 (multiplate-forme, 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 application 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

- Cours de conception.

- Tutoriel sur le développement dirigé par les tests (tutoriel avancé).

- Refactorisation.

- Développement de logiciel.

II - PHPUnit

PHPUnit est un framework de tests unitaires open source.

Quelques qualités…

- Une syntaxe simple, facile à comprendre et à retenir.

- Un grand nombre de méthodes de tests.

- Organisation et exécution des tests 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 formats XML, JSON, TAP ou dans une base de données.

PHPUnit est pris en charge nativement dans les IDE suivants :

- 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

ou télécharger et installer manuellement le package via : http://pear.phpunit.de/get/

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\).

Commande

phpunit --help

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

Journalise 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 source.

--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 gabarits

Le générateur de gabarits (générateur de templates, ou encore générateur de squelettes) 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
// Call MyClassTest::main() if this source file is executed directly.
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'MyClassTest::main');
}

require_once 'PHPUnit/Framework.php';

require_once 'MyClass.php';

/**
* Test class for MyClass.
* Generated by PHPUnit on 2007-08-11 at 20:01:00.
*/
class MyClassTest extends PHPUnit_Framework_TestCase {
/**
* Runs the test methods of this class.
*
* @access public
* @static
*/
public static function main() {
require_once 'PHPUnit/TextUI/TestRunner.php';

$suite  = new PHPUnit_Framework_TestSuite('MyClassTest');
$result = PHPUnit_TextUI_TestRunner::run($suite);
}

/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
*
* @access protected
*/
protected function setUp() {
}

/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*
* @access protected
*/
protected function tearDown() {
}

/**
* @todo Implement testMyMethod().
*/
public function testMyMethod() {
// Remove the following lines when you implement this test.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
}

// Call MyClassTest::main() if this source file is executed directly.
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étail 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 :

@assert (argument1 [, argument2, ...]) operator value

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
{
/**
* @assert (1) > 2
*/
public function myMethod($value)
{
return $value;
}
}

Méthode de test générée :

/**
* Generated from @assert (1) > 2.
*/
public function testMyMethod() {
$constraint = $this->greaterThan(2);
$object     = new myClass;
$value      = $object->myMethod(1);
$this->assertThat($value, $constraint);
}

Ces annotations 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 gabarits 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 autres 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éressent 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

phpunit 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

php AllTests.php

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

1. Le squelette de la classe MyFile est généré à partir du diagramme de classe susmentionné.

2. 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
// Call MyFileTest::main() if this source file is executed directly.
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'MyFileTest::main');
}

require_once 'PHPUnit/Framework.php';

require_once 'MyFile.php';

/**
* Test class for MyFile.
* Generated by PHPUnit on 2007-08-12 at 19:25:50.
*/
class MyFileTest extends PHPUnit_Framework_TestCase {
/**
* Runs the test methods of this class.
*
* @access public
* @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());
}
}

// Call MyFileTest::main() if this source file is executed directly.
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);
}
}

/**
* Affecte un nom de fichier (de 1 à 32 caractères alphanumériques, trait d'union et sous-tiret compris).
* @param string $fileName
* @return boolean
*/
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;
}

/**
* Affecte une extension de fichier (de 1 à 5 caractères alphanumériques).
* @param string $extension
* @return boolean
*/
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;
}

/**
* Retourne le nom de fichier.
*
* @return string
*/
public function getFilename()
{
return $this->fileName;
}

/**
* Retourne l'extension du fichier
*
* @return string
*/
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

- Site officiel de PHPUnit.

Ressources Developpez.com

- PHPEclipse : Programmez librement pour le Web, par Jean-Pierre Grossglauser.

- PHPEdit, un IDE complet pour PHP, par Guillaume Rossolini.

- Compte-rendu de la Conférence Internationale PHP 2006, par Guillaume Rossolini.