Créer un nouveau de scénario de test
Si vous débutez avec les tests unitaires, il est recommandé d'essayer le code au fur et à mesure. Il n'y pas grand chose à taper et vous sentirez le rythme de la programmation pilotée par les tests.
Pour exécuter les exemples tels quels, vous aurez besoin de créer un nouveau répertoire et d'y installer trois dossiers : classes, tests et temp. Dézippez le framework SimpleTest dans le dossier tests et assurez vous que votre serveur web puisse atteindre ces endroits.
Un nouveau scénario de test
L'exemple dans l'introduction rapide comprenait les tests unitaires d'une simple classe de log. Dans ce tutorial à propos de Simple Test, je vais essayer de raconter toute l'histoire du développement de cette classe. Cette classe PHP est courte et simple : au cours de cette introduction, elle recevra beaucoup plus d'attention que dans le cadre d'un développement de production. Nous verrons que derrière son apparente simplicité se cachent des choix de conception étonnamment difficiles.
Peut-être que ces choix sont trop difficiles ? Plutôt que d'essayer de penser à tout en amont, je vais commencer par poser une exigence : nous voulons écrire des messages dans un fichier. Ces messages doivent être ajoutés en fin de fichier s'il existe. Plus tard nous aurons besoin de priorités, de filtres et d'autres choses encore, mais nous plaçons l'écriture dans un fichier au coeur de nos préoccupations. Nous ne penserons à rien d'autres par peur de confusion. OK, commençons par écrire un test...
<?php if (! defined('SIMPLE_TEST')) { define('SIMPLE_TEST', 'simpletest/'); } require_once(SIMPLE_TEST . 'autorun.php'); class TestOfLogging extends UnitTestCase { function testCreatingNewFile() { } } $test = &new TestOfLogging(); $test->run(new HtmlReporter()); ?>Pas à pas, voici ce qu'il veut dire.
La constante SIMPLE_TEST
contient
le chemin vers les classes de Simple Test à partir de ce fichier.
Les classes pourraient être placées dans le path
du fichier php.ini mais si vous êtes sur un serveur mutualisé,
vous n'y aurez probablement pas accès.
Pour que tout le monde soit content,
le chemin est déclaré explicitement dans le script de test.
Plus tard nous verrons comment tout finira au même endroit.
Qu'est-ce donc que ce fichier autorun.php ? Les bibliothèques de SimpleTest sont une boîte à outil pour créer votre propre suite de tests standardisés. Elles peuvent être utilisées "telles que" sans problème, mais sont constituées par des composants qui doivent être assemblés. autorun.php est un composant spécial : il fournit les parties "testeur unitaire" et "affichage". Il attrape les classes de test et les lance automagiquement.
Il est probable que vous en viendrez à écrire votre propre affichage
et ajouter cette version par défaut est optionnel.
SimpleTest inclut une classe d'affichage utilisable - et basique -
appelée HtmlReporter
.
Sur des tests, elle peut enregistrer débuts, fins, erreurs, succès et échec.
Elle affiche ces informations au plus vite, au cas où le code
du test fait planter le script et masque la source d'échec.
Les tests eux-mêmes sont rassemblés dans une classe de scénario de test.
Cette dernière est typiquement une extension de
la classe UnitTestCase
.
Quand le test est exécuté, elle cherche les méthodes
commençant par "test" et les lancent.
Notre seule méthode de test pour l'instant est appellée
testCreatingNewFile()
mais elle est encore vide.
Une méthode vide ne fait rien. Nous avons besoin d'y placer du code.
La classe UnitTestCase
génère des évènements
de test à son exécution :
ces évènements sont envoyés vers un observateur.
Et pour ajouter du code de test...
<?php if (! defined('SIMPLE_TEST')) { define('SIMPLE_TEST', 'simpletest/'); } require_once(SIMPLE_TEST . 'autorun.php'); require_once('../classes/log.php'); class TestOfLogging extends UnitTestCase { function testCreatingNewFile() { @unlink('../temp/test.log'); $log = new Log('../temp/test.log'); $log->message('Should write this to a file'); $this->assertTrue(file_exists('../temp/test.log')); } } $test = &new TestOfLogging(); $test->run(new HtmlReporter()); ?>
Vous pensez probablement que ça représente beaucoup de code pour un unique test et je suis d'accord avec vous. Ne vous inquiétez pas. Il s'agit d'un coût fixe et à partir de maintenant nous pouvons ajouter des tests : une ligne ou presque à chaque fois. Parfois moins en utilisant des artefacts de test que nous découvrirons plus tard.
Nous devons maintenant prendre nos premières décisions. Notre fichier de test s'appelle log_test.php (n'importe quel nom ferait l'affaire) : nous le plaçons dans un dossier appelé tests (partout ailleurs serait aussi bien). Notre fichier de code s'appelle log.php : c'est son contenu que nous allons tester. Je l'ai placé dans notre dossier classes : cela veut-il dire que nous construisons une classe ?
Pour cet exemple, la réponse est oui, mais le testeur unitaire n'est pas restreint aux tests de classe. C'est juste que le code orienté objet est plus facile à dépecer et à remodeler. Ce n'est pas par hasard si la conduite de tests fins via les tests unitaires est apparue au sein de la communauté OO.
Le test en lui-même est minimal. Tout d'abord il élimine
tout autre fichier de test qui serait encore présent.
Les décisions de conception arrivent ensuite en rafale.
Notre classe s'appelle Log
:
elle passe le chemin du fichier au constructeur.
Nous créons le log et nous lui envoyons aussitôt
un message en utilisant la méthode message()
.
L'originalité dans le nommage n'est pas
une caractéristique désirable chez un développeur informatique :
c'est triste mais c'est comme ça.
La plus petite unité d'un test mmm... heu... unitaire est l'assertion.
Ici nous voulons nous assurer que le fichier log
auquel nous venons d'envoyer un message a bel et bien été créé.
UnitTestCase::assertTrue()
enverra
un évènement réussite si la condition évaluée est vraie
ou un échec dans le cas contraire.
Nous pouvons avoir un ensemble d'assertions différentes
et encore plus si nous étendons
nos scénarios de test classique. Voici la liste...
assertTrue($x) | Echoue si $x est faux |
assertFalse($x) | Echoue si $x est vrai |
assertNull($x) | Echoue si $x est initialisé |
assertNotNull($x) | Echoue si $x n'est pas initialisé |
assertIsA($x, $t) | Echoue si $x n'est pas de la classe ou du type $t |
assertEqual($x, $y) | Echoue si $x == $y est faux |
assertNotEqual($x, $y) | Echoue si $x == $y est vrai |
assertIdentical($x, $y) | Echoue si $x === $y est faux |
assertNotIdentical($x, $y) | Echoue si $x === $y est vrai |
assertReference($x, $y) | Echoue sauf si $x et $y sont la même variable |
assertCopy($x, $y) | Echoue si $x et $y sont la même variable |
assertWantedPattern($p, $x) | Echoue sauf si l'expression rationnelle $p capture $x |
assertNoUnwantedPattern($p, $x) | Echoue si l'expression rationnelle $p capture $x |
assertNoErrors() | Echoue si une erreur PHP arrive |
assertError($x) | Echoue si aucune erreur ou message incorrect de PHP n'arrive |
Nous sommes désormais prêt à lancer notre script de test en le passant dans le navigateur. Qu'est-ce qui devrait arriver ? Il devrait planter...
Mais attendez une minute, c'est idiot ! Ne me dites pas qu'il faut créer un test sans écrire le code à tester auparavant...
Développement piloté par les tests
Co-inventeur de l'Extreme Programming, Kent Beck a lancé un autre manifeste. Le livre est appelé Test Driven Development (Développement Piloté par les Tests) ou TDD et élève les tests unitaires à une position élevée de la conception. En quelques mots, vous écrivez d'abord un petit test et seulement ensuite le code qui passe ce test. N'importe quel bout de code. Juste pour qu'il passe.
Vous écrivez un autre test et puis de nouveau du code qui passe. Vous aurez alors un peu de duplication et généralement du code pas très propre. Vous remaniez (factorisez) ce code-là en vous assurant que les tests continuent à passer : vous ne pouvez rien casser. Une fois que le code est le plus propre possible vous êtes prêt à ajouter des nouvelles fonctionnalités. Il suffit juste de rajouter des nouveaux tests et de recommencer le cycle une nouvelle fois.
Il s'agit d'une approche assez radicale et j'ai parfois l'impression qu'elle est incomplète. Mais il s'agit d'un moyen efficace pour expliquer un testeur unitaire ! Il se trouve que nous avons un test qui échoue, pour ne pas dire qu'il plante : l'heure est venue d'écrire du code dans log.php...
<?php class Log { function Log($file_path) { } function message($message) { } } ?>Il s'agit là du minimum que nous puissions faire pour éviter une erreur fatale de PHP. Et maintenant la réponse devient...
testoflogging
Fail: testcreatingnewfile->True assertion failed.class TestOfLogging extends UnitTestCase { function TestOfLogging() { $this->UnitTestCase('Log class test'); } function testCreatingNewFile() { @unlink('../temp/test.log'); $log = new Log('../temp/test.log'); $log->message('Should write this to a file'); $this->assertTrue(file_exists('../temp/test.log'), 'File created'); } }Ce qui donne...
Log class test
Fail: testcreatingnewfile->File created.Les messages d'un test comme ceux-ci ressemblent à bien des égards à des commentaires de code. Certains ne jurent que par eux, d'autres au contraire les bannissent purement et simplement en les considérant aussi encombrants qu'inutiles. Pour ma part, je me situe quelque part au milieu.
Pour que le test passe, nous pourrions nous contenter
de créer le fichier dans le constructeur de Log
.
Cette technique "en faisant semblant" est très utile
pour vérifier que le test fonctionne pendant les passages difficiles.
Elle le devient encore plus si vous sortez d'un passage
avec des tests ayant échoués et que vous voulez juste vérifier
de ne pas avoir oublié un truc bête.
Nous n'allons pas aussi lentement donc...
<?php class Log { var $_file_path; function Log($file_path) { $this->_file_path = $file_path; } function message($message) { $file = fopen($this->_file_path, 'a'); fwrite($file, $message . "\n"); fclose($file); } } ?>Au total, pas moins de 4 échecs ont été nécessaire pour passer à l'étape suivante. Je n'avais pas créé le répertoire temporaire, je ne lui avais pas donné les droits d'écriture, j'avais une coquille et je n'avais pas non plus ajouté ce nouveau répertoire dans CVS. N'importe laquelle de ces erreurs aurait pu m'occuper pendant plusieurs heures si elle était apparue plus tard mais c'est bien pour ces cas là qu'on teste. Avec les corrections adéquates, ça donne...
Log class test
Peut-être n'aimez-vous pas le style plutôt minimal de l'affichage. Les succès ne sont pas montrés par défaut puisque généralement vous n'avez pas besoin de plus d'information quand vous comprenez effectivement ce qui se passe. Dans le cas contraire, pensez à écrire d'autres tests.
D'accord, c'est assez strict. Si vous voulez aussi voir
les succès alors vous pouvez
créer une sous-classe
de HtmlReporter
et l'utiliser pour les tests.
Même moi j'aime bien ce confort parfois.
Les tests comme documentation
Il y a une nuance ici. Nous ne voulons pas créer de fichier avant d'avoir effectivement envoyé de message. Plutôt que d'y réfléchir trop longtemps, nous allons juste ajouter un test pour ça.
class TestOfLogging extends UnitTestCase { function TestOfLogging() { $this->UnitTestCase('Log class test'); } function testCreatingNewFile() { @unlink('../temp/test.log'); $log = new Log('../temp/test.log'); $this->assertFalse(file_exists('../temp/test.log'), 'No file created before first message'); $log->message('Should write this to a file'); $this->assertTrue(file_exists('../temp/test.log'), 'File created'); } }...et découvrir que ça marche déjà...
Log class test
Devrions-nous supprimer le fichier temporaire à la fin du test ? Par habitude, je le fais une fois que j'en ai terminé avec la méthode de test et qu'elle marche. Je n'ai pas envie de valider du code qui laisse des restes de fichiers de test traîner après un test. Mais je ne le fais pas non plus pendant que j'écris le code. Peut-être devrais-je, mais parfois j'ai besoin de voir ce qui se passe : on retrouve cet aspect confort évoqué plus haut.
Dans un véritable projet, nous avons habituellement plus qu'un unique scénario de test : c'est pourquoi nous allons regarder comment grouper des tests dans des suites de tests.