1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
|
<?xml version="1.0" encoding="ISO-8859-1" ?>
<page title="Crer un nouveau de scnario de test" here="Tutorial : les tests unitaires en PHP">
<long_title>Tutorial sur les tests unitaires en PHP - Crer un exemple de scnario de test en PHP</long_title>
<content>
<p>
Si vous dbutez 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 pilote par les tests.
</p>
<p>
Pour excuter les exemples tels quels, vous aurez besoin de crer un nouveau rpertoire et d'y installer trois dossiers : <em>classes</em>, <em>tests</em> et <em>temp</em>. Dzippez le framework <a href="simple_test.php">SimpleTest</a> dans le dossier <em>tests</em> et assurez vous que votre serveur web puisse atteindre ces endroits.
</p>
<p>
<a class="target" name="nouveau"><h2>Un nouveau scnario de test</h2></a>
</p>
<p>
L'exemple dans <a local="{{simple_test}}">l'introduction rapide</a> 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 dveloppement 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 dveloppement de production. Nous verrons que derrire son apparente simplicit se cachent des choix de conception tonnamment difficiles.
</p>
<p>
Peut-tre que ces choix sont trop difficiles ? Plutt 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 ajouts en fin de fichier s'il existe. Plus tard nous aurons besoin de priorits, de filtres et d'autres choses encore, mais nous plaons l'criture dans un fichier au coeur de nos proccupations. Nous ne penserons rien d'autres par peur de confusion. OK, commenons par crire un test...
<php><![CDATA[
<strong><?php
if (! defined('SIMPLE_TEST')) {
define('SIMPLE_TEST', 'simpletest/');
}
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {
$this->UnitTestCase();
}
function testCreatingNewFile() {
}
}
$test = &new TestOfLogging();
$test->run(new HtmlReporter());
?></strong>
]]></php>
Pas pas, voici ce qu'il veut dire.
</p>
<p>
La constante <code>SIMPLE_TEST</code> contient le chemin vers les classes de Simple Test partir de ce fichier. Les classes pourraient tre places dans le path du fichier <em>php.ini</em> mais si vous tes sur un serveur mutualis, vous n'y aurez probablement pas accs. Pour que tout le monde soit content, le chemin est dclar explicitement dans le script de test. Plus tard nous verrons comment tout finira au mme endroit.
</p>
<p>
Demander la librairie <em>unit_tester.php</em> est assez vident mais qu'est-ce que ce fichier <em>reporter.php</em> ? Les librairies Simple Test sont une bote outils pour crer votre propre suite de tests standardiss. Elles peuvent tre utilises "telles quelles" sans problme, mais elles sont constitues de plusieurs lments qui ont besoin d'tre assembls les uns aux autres. Le composant pour l'affichage est situ dans <em>reporter.php</em>. Probablement qu'un jour vous crirez le vtre : c'est pourquoi son inclusion est optionnelle. Simple Test contient une classe d'affichage, fonctionnelle mais basique : elle s'appelle <code>HtmlReporter</code>. Elle peut enregistrer des informations sur les tests : dbut, fin, erreur, russite ou chec. Elle affiche cette information le plus rapidement possible au cas o le code de test planterait et masquerait le lieu de l'chec.
</p>
<p>
Les tests eux-mmes sont rassembls dans une classe de scnario de test. Cette dernire est typiquement une extension de la classe <code>UnitTestCase</code>. Quand le test est excut, elle cherche les mthodes commenant par "test" et les lancent. Notre seule mthode de test pour l'instant est appelle <code>testCreatingNewFile()</code> mais elle est encore vide.
</p>
<p>
Une mthode vide ne fait rien. Nous avons besoin d'y placer du code. La classe <code>UnitTestCase</code> gnre des vnements de test son excution : ces vnements sont envoys vers un observateur. La mthode <code>UnitTestCase::run()</code> lancent tous les tests de la classe.
</p>
<p>
Et pour ajouter du code de test...
<php><![CDATA[
<?php
if (! defined('SIMPLE_TEST')) {
define('SIMPLE_TEST', 'simpletest/');
}
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');<strong>
require_once('../classes/log.php');</strong>
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {
$this->UnitTestCase();
}
function testCreatingNewFile() {<strong>
@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'));</strong>
}
}
$test = &new TestOfLogging();
$test->run(new HtmlReporter());
?>
]]></php>
</p>
<p>
Vous pensez probablement que a reprsente beaucoup de code pour un unique test et je suis d'accord avec vous. Ne vous inquitez pas. Il s'agit d'un cot 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 dcouvrirons plus tard.
</p>
<p>
Nous devons maintenant prendre nos premires dcisions. Notre fichier de test s'appelle <em>log_test.php</em> (n'importe quel nom ferait l'affaire) : nous le plaons dans un dossier appel <em>tests</em> (partout ailleurs serait aussi bien). Notre fichier de code s'appelle <em>log.php</em> : c'est son contenu que nous allons tester. Je l'ai plac dans notre dossier <em>classes</em> : cela veut-il dire que nous construisons une classe ?
</p>
<p>
Pour cet exemple, la rponse 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 dpecer 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.
</p>
<p>
Le test en lui-mme est minimal. Tout d'abord il limine tout autre fichier de test qui serait encore prsent. Les dcisions de conception arrivent ensuite en rafale. Notre classe s'appelle <code>Log</code> : elle passe le chemin du fichier au constructeur. Nous crons le log et nous lui envoyons aussitt un message en utilisant la mthode <code>message()</code>. L'originalit dans le nommage n'est pas une caractristique dsirable chez un dveloppeur informatique : c'est triste mais c'est comme a.
</p>
<p>
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. <code>UnitTestCase::assertTrue()</code> enverra un vnement russite si la condition value est vraie ou un chec dans le cas contraire. Nous pouvons avoir un ensemble d'assertions diffrentes et encore plus si nous tendons nos scnarios de test classique. Voici la liste...
<table><tbody>
<tr><td><code>assertTrue($x)</code></td><td>Echoue si $x est faux</td></tr>
<tr><td><code>assertFalse($x)</code></td><td>Echoue si $x est vrai</td></tr>
<tr><td><code>assertNull($x)</code></td><td>Echoue si $x est initialis</td></tr>
<tr><td><code>assertNotNull($x)</code></td><td>Echoue si $x n'est pas initialis</td></tr>
<tr><td><code>assertIsA($x, $t)</code></td><td>Echoue si $x n'est pas de la classe ou du type $t</td></tr>
<tr><td><code>assertEqual($x, $y)</code></td><td>Echoue si $x == $y est faux</td></tr>
<tr><td><code>assertNotEqual($x, $y)</code></td><td>Echoue si $x == $y est vrai</td></tr>
<tr><td><code>assertIdentical($x, $y)</code></td><td>Echoue si $x === $y est faux</td></tr>
<tr><td><code>assertNotIdentical($x, $y)</code></td><td>Echoue si $x === $y est vrai</td></tr>
<tr><td><code>assertReference($x, $y)</code></td><td>Echoue sauf si $x et $y sont la mme variable</td></tr>
<tr><td><code>assertCopy($x, $y)</code></td><td>Echoue si $x et $y sont la mme variable</td></tr>
<tr><td><code>assertWantedPattern($p, $x)</code></td><td>Echoue sauf si l'expression rationnelle $p capture $x</td></tr>
<tr><td><code>assertNoUnwantedPattern($p, $x)</code></td><td>Echoue si l'expression rationnelle $p capture $x</td></tr>
<tr><td><code>assertNoErrors()</code></td><td>Echoue si une erreur PHP arrive</td></tr>
<tr><td><code>assertError($x)</code></td><td>Echoue si aucune erreur ou message incorrect de PHP n'arrive</td></tr>
</tbody></table>
</p>
<p>
Nous sommes dsormais prt lancer notre script de test en le passant dans le navigateur. Qu'est-ce qui devrait arriver ? Il devrait planter...
<div class="demo">
<b>Fatal error</b>: Failed opening required '../classes/log.php' (include_path='') in <b>/home/marcus/projects/lastcraft/tutorial_tests/Log/tests/log_test.php</b> on line <b>7</b>
</div>
La raison ? Nous n'avons pas encore cr <em>log.php</em>.
</p>
<p>
Mais attendez une minute, c'est idiot ! Ne me dites pas qu'il faut crer un test sans crire le code tester auparavant...
</p>
<p>
<a class="target" name="tdd"><h2>Dveloppement pilot par les tests</h2></a>
</p>
<p>
Co-inventeur de l'<a href="http://www.extremeprogramming.org/">Extreme Programming</a>, Kent Beck a lanc un autre manifeste. Le livre est appel <a href="http://www.amazon.com/exec/obidos/tg/detail/-/0321146530/ref=lib_dp_TFCV/102-2696523-7458519?v=glance&s=books&vi=reader#reader-link">Test Driven Development</a> (Dveloppement Pilot par les Tests)
ou TDD et lve les tests unitaires une position leve 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.
</p>
<p>
Vous crivez un autre test et puis de nouveau du code qui passe. Vous aurez alors un peu de duplication et gnralement du code pas trs 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 prt ajouter des nouvelles fonctionnalits. Il suffit juste de rajouter des nouveaux tests et de recommencer le cycle une nouvelle fois.
</p>
<p>
Il s'agit d'une approche assez radicale et j'ai parfois l'impression qu'elle est incomplte. 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 <em>log.php</em>...
<php><![CDATA[
<strong><?php
class Log {
function Log($file_path) {
}
function message($message) {
}
}
?></strong>
]]></php>
Il s'agit l du minimum que nous puissions faire pour viter une erreur fatale de PHP. Et maintenant la rponse devient...
<div class="demo">
<h1>testoflogging</h1>
<span class="fail">Fail</span>: testcreatingnewfile->True assertion failed.<br />
<div style="padding: 8px; margin-top: 1em; background-color: red; color: white;">1/1 test cases complete.
<strong>0</strong> passes and <strong>1</strong> fails.</div>
</div>
"testoflogging" a chou. Parmi les dfauts de PHP on trouve cette fcheuse tendance transformer intrieurement les noms de classes et de mthodes en minuscules. SimpleTest utilise ces noms par dfaut pour dcrire les tests mais nous pouvons les remplacer par nos propres noms.
<php><![CDATA[
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {<strong>
$this->UnitTestCase('Log class test');</strong>
}
function testCreatingNewFile() {
@unlink('../temp/test.log');
$log = new Log('../temp/test.log');
$log->message('Should write this to a file');<strong>
$this->assertTrue(file_exists('../temp/test.log'), 'File created');</strong>
}
}
]]></php>
Ce qui donne...
<div class="demo">
<h1>Log class test</h1>
<span class="fail">Fail</span>: testcreatingnewfile->File created.<br />
<div style="padding: 8px; margin-top: 1em; background-color: red; color: white;">1/1 test cases complete.
<strong>0</strong> passes and <strong>1</strong> fails.</div>
</div>
Par contre pour le nom des mthodes il n'y a rien faire, dsol.
</p>
<p>
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 considrant aussi encombrants qu'inutiles. Pour ma part, je me situe quelque part au milieu.
</p>
<p>
Pour que le test passe, nous pourrions nous contenter de crer le fichier dans le constructeur de <code>Log</code>. Cette technique "en faisant semblant" est trs utile pour vrifier que le test fonctionne pendant les passages difficiles. Elle le devient encore plus si vous sortez d'un passage avec des tests ayant chous et que vous voulez juste vrifier de ne pas avoir oubli un truc bte. Nous n'allons pas aussi lentement donc...
<php><![CDATA[
<?php
class Log {<strong>
var $_file_path;</strong>
function Log($file_path) {<strong>
$this->_file_path = $file_path;</strong>
}
function message($message) {<strong>
$file = fopen($this->_file_path, 'a');
fwrite($file, $message . "\n");
fclose($file);</strong>
}
}
?>
]]></php>
Au total, pas moins de 4 checs ont t ncessaire pour passer l'tape suivante. Je n'avais pas cr le rpertoire 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 rpertoire 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 adquates, a donne...
<div class="demo">
<h1>Log class test</h1>
<div style="padding: 8px; margin-top: 1em; background-color: green; color: white;">1/1 test cases complete.
<strong>1</strong> passes and <strong>0</strong> fails.</div>
</div>
a marche!
</p>
<p>
Peut-tre n'aimez-vous pas le style plutt minimal de l'affichage. Les succs ne sont pas montrs par dfaut puisque gnralement 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.
</p>
<p>
D'accord, c'est assez strict. Si vous voulez aussi voir les succs alors vous pouvez <a local="display_subclass_tutorial">crer une sous-classe de <code>HtmlReporter</code></a> et l'utiliser pour les tests. Mme moi j'aime bien ce confort parfois.
</p>
<p>
<a class="target" name="doc"><h2>Les tests comme documentation</h2></a>
</p>
<p>
Il y a une nuance ici. Nous ne voulons pas crer de fichier avant d'avoir effectivement envoy de message. Plutt que d'y rflchir trop longtemps, nous allons juste ajouter un test pour a.
<php><![CDATA[
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {
$this->UnitTestCase('Log class test');
}
function testCreatingNewFile() {
@unlink('../temp/test.log');
$log = new Log('../temp/test.log');<strong>
$this->assertFalse(file_exists('../temp/test.log'), 'No file created before first message');</strong>
$log->message('Should write this to a file');
$this->assertTrue(file_exists('../temp/test.log'), 'File created');
}
}
]]></php>
...et dcouvrir que a marche dj...
<div class="demo">
<h1>Log class test</h1>
<div style="padding: 8px; margin-top: 1em; background-color: green; color: white;">1/1 test cases complete.
<strong>2</strong> passes and <strong>0</strong> fails.</div>
</div>
En fait je savais que a allait tre le cas. J'ajoute ce test de confirmation tout d'abord pour garder l'esprit tranquille, mais aussi pour documenter ce comportement. Ce petit test supplmentaire dans son contexte en dit plus long qu'un scnario utilisateur d'une douzaine de lignes ou qu'un diagramme UML complet. Que la suite de tests devienne une source de documentation est un effet secondaire assez agrable.
</p>
<p>
Devrions-nous supprimer le fichier temporaire la fin du test ? Par habitude, je le fais une fois que j'en ai termin avec la mthode de test et qu'elle marche. Je n'ai pas envie de valider du code qui laisse des restes de fichiers de test traner aprs 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.
</p>
<p>
Dans un vritable projet, nous avons habituellement plus qu'un unique scnario de test : c'est pourquoi nous allons regarder comment <a local="group_test_tutorial">grouper des tests dans des suites de tests</a>.
</p>
</content>
<internal>
<link>Crer un <a href="#nouveau">nouveau scnario de test</a>.</link>
<link>Le <a href="#tdd">Dveloppement Pilot par les Tests</a> en PHP.</link>
<link>Les <a href="#doc">tests comme documentation</a> est un des nombreux effets secondaires.</link>
</internal>
<external>
<link>
La <a href="http://junit.sourceforge.net/doc/faq/faq.htm">FAQ de JUnit</a> contient plein de conseils judicieux sur les tests.
</link>
<link>
<a href="group_test_tutorial.php">Ensuite</a> vient "comment grouper des scnarios de tests ensemble".
</link>
<link>
Vous aurez besoin du <a href="simple_test.php">framework de test SimpleTest</a> pour ces exemples.
</link>
</external>
<meta>
<keywords>
dveloppement logiciel,
programmation php,
outils de dveloppement logiciel,
tutorial php,
scripts php gratuits,
architecture,
ressouces php,
objets fantaisie,
junit,
test php,
test unitaire,
test php automatis,
tutorial de scnarios de test,
explication d'un scnario de test unitaire,
exemple de test unitaire
</keywords>
</meta>
</page>
|