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
|
<?xml version="1.0" encoding="ISO-8859-1" ?>
<page title="Les frontires de l'application" here="Les frontires de l'application">
<long_title>
Tutorial de tests unitaires PHP - Organiser les tests unitaires et les scnarios de test de classe frontire
</long_title>
<content>
<p>
Vous pensez probablement que nous avons dsormais puis les modifications sur la classe <code>Log</code> et qu'il n'y a plus rien ajouter. Sauf que les choses ne sont jamais simples avec la Programmation Orient Objet. Vous pensez comprendre un problme et un nouveau cas arrive : il dfie votre point de vue et vous conduit vers une analyse encore plus profonde. Je pensais comprendre la classe de log et que seule la premire page du tutorial l'utiliserait. Aprs a, je serais pass quelque chose de plus compliqu. Personne n'est plus surpris que moi de ne pas l'avoir boucle. En fait je pense que je viens peine de me rendre compte de ce qu'un loggueur fait.
</p>
<p>
<a class="target" name="variation"><h2>Variations sur un log</h2></a>
</p>
<p>
Supposons que nous ne voulons plus seulement enregistrer les logs vers un fichier. Nous pourrions vouloir les afficher l'cran, les envoyer au daemon <em>syslog</em> d'Unix(tm) via un socket. Comment s'accommoder de tels changements ?
</p>
<p>
Le plus simple est de crer des sous-classes de <code>Log</code> qui crasent la mthode <code>message()</code> avec les nouvelles versions. Ce systme fonctionne bien court terme, sauf qu'il a quelque chose de subtilement mais foncirement erron. Supposons que nous crions ces sous-classes et que nous ayons des loggueurs crivant vers un fichier, sur l'cran et via le rseau. Trois classes en tout : a fonctionne. Maintenant supposons que nous voulons ajouter une nouvelle classe de log qui ajoute un filtrage par priorit des messages, ne laissant passer que les messages d'un certain type, le tout suivant un fichier de configuration.
</p>
<p>
Nous sommes coincs. Si nous crons de nouvelles sous-classes, nous devons le faire pour l'ensemble des trois classes, ce qui nous donnerait six classes. L'envergure de la duplication est horrible.
</p>
<p>
Alors, est-ce que vous tes en train de souhaiter que PHP ait l'hritage multiple ? Effectivement, cela rduirait l'ampleur de la tche court terme, mais aussi compliquerait quelque chose qui devrait tre une classe trs simple. L'hritage multiple, mme support, devrait tre utilis avec le plus grand soin car toutes sortes d'enchevtrements peuvent en dcouler. En fait ce soudain besoin nous dit quelque chose d'autre - peut-tre que notre erreur si situe au niveau de la conception.
</p>
<p>
Qu'est-ce que doit faire un loggueur ? Est-ce qu'il envoie un message vers un fichier ? A l'cran ? Via le rseau ? Non. Il envoie un message, point final. La cible de ses messages peut tre slectionne l'initialisation du log, mais aprs a pas touche : le loggueur doit pouvoir combiner et formater les lments du message puisque tel est son vritable boulot. Prsumer que la cible fut un nom de fichier tait une belle paire d'oeillres.
</p>
<p>
<a class="target" name="scripteur"><h2>Abstraire un fichier vers un scripteur</h2></a>
</p>
<p>
La solution de cette mauvaise passe est un classique. Tout d'abord nous encapsulons la variation de la classe : cela ajoute un niveau d'indirection. Au lieu d'introduire le nom du fichier comme une chane, nous l'introduisons comme "cette chose vers laquelle on crit" et que nous appelons un <code>Writer</code>. Retour aux tests...
<php><![CDATA[
<?php
require_once('../classes/log.php');
require_once('../classes/clock.php');<strong>
require_once('../classes/writer.php');</strong>
Mock::generate('Clock');
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {
$this->UnitTestCase('Log class test');
}
function setUp() {
@unlink('../temp/test.log');
}
function tearDown() {
@unlink('../temp/test.log');
}
function getFileLine($filename, $index) {
$messages = file($filename);
return $messages[$index];
}
function testCreatingNewFile() {<strong>
$log = new Log(new FileWriter('../temp/test.log'));</strong>
$this->assertFalse(file_exists('../temp/test.log'), 'Created before message');
$log->message('Should write this to a file');
$this->assertTrue(file_exists('../temp/test.log'), 'File created');
}
function testAppendingToFile() {<strong>
$log = new Log(new FileWriter('../temp/test.log'));</strong>
$log->message('Test line 1');
$this->assertWantedPattern(
'/Test line 1/',
$this->getFileLine('../temp/test.log', 0));
$log->message('Test line 2');
$this->assertWantedPattern(
'/Test line 2/',
$this->getFileLine('../temp/test.log', 1));
}
function testTimestamps() {
$clock = &new MockClock($this);
$clock->setReturnValue('now', 'Timestamp');<strong>
$log = new Log(new FileWriter('../temp/test.log'));</strong>
$log->message('Test line', &$clock);
$this->assertWantedPattern(
'/Timestamp/',
$this->getFileLine('../temp/test.log', 0),
'Found timestamp');
}
}
?>
]]></php>
Je vais parcourir ces tests pas pas pour ne pas ajouter trop de confusion. J'ai remplac les noms de fichier par une classe imaginaire <code>FileWriter</code> en provenance d'un fichier <em>classes/writer.php</em>. Par consquent les tests devraient planter puisque nous n'avons pas encore crit ce scripteur. Doit-on le faire maintenant ?
</p>
<p>
Nous pourrions, mais ce n'est pas oblig. Par contre nous avons besoin de crer l'interface, ou alors il ne sera pas possible de la simuler. Au final <em>classes/writer.php</em> ressemble ...
<php><![CDATA[
<?php
class FileWriter {
function FileWriter($file_path) {
}
function write($message) {
}
}
?>
]]></php>
Nous avons aussi besoin de modifier la classe <code>Log</code>...
<php><![CDATA[
<?php
require_once('../classes/clock.php');<strong>
require_once('../classes/writer.php');</strong>
class Log {<strong>
var $_writer;</strong>
function Log(<strong>&$writer</strong>) {<strong>
$this->_writer = &$writer;</strong>
}
function message($message, $clock = false) {
if (! is_object($clock)) {
$clock = new Clock();
}<strong>
$this->_writer->write("[" . $clock->now() . "] $message");</strong>
}
}
?>
]]></php>
Il n'y a pas grand chose qui n'ait pas chang y compris dans la plus petite de nos classes. Dsormais les tests s'excutent mais ne passent pas, moins que nous ajoutions du code dans le scripteur. Alors que faisons nous ?
</p>
<p>
Nous pourrions commencer par crire des tests et dvelopper la classe <code>FileWriter</code> paralllement, mais lors de cette tape nos tests de <code>Log</code> continueraient d'chouer et de nous distraire. En fait nous n'en avons pas besoin.
</p>
<p>
Une partie de notre objectif est de librer la classe du loggueur de l'emprise du systme de fichiers et il existe un moyen d'y arriver. Tout d'abord nous crons le fichier <em>tests/writer_test.php</em> de manire avoir un endroit pour placer notre code test en provenance de <em>log_test.php</em> et que nous allons brasser. Sauf que je ne vais pas l'ajouter dans le fichier <em>all_tests.php</em> pour l'instant puisque qu'il s'agit de la partie de log que nous sommes en train d'aborder.
</p>
<p>
Nous enlevons tous les test de <em>log_test.php</em> qui ne sont pas strictement en lien avec le journal et nous les gardons bien prcieusement dans <em>writer_test.php</em> pour plus tard. Nous allons aussi simuler le scripteur pour qu'il n'crive pas rellement dans un fichier...
<php><![CDATA[
<?php
require_once('../classes/log.php');
require_once('../classes/clock.php');
require_once('../classes/writer.php');
Mock::generate('Clock');<strong>
Mock::generate('FileWriter');</strong>
class TestOfLogging extends UnitTestCase {
function TestOfLogging() {
$this->UnitTestCase('Log class test');
}<strong>
function testWriting() {
$clock = &new MockClock($this);
$clock->setReturnValue('now', 'Timestamp');
$writer = &new MockFileWriter($this);
$writer->expectArguments('write', array('[Timestamp] Test line'));
$writer->expectCallCount('write', 1);
$log = &new Log($writer);
$log->message('Test line', &$clock);
$writer->tally();
}</strong>
}
?>
]]></php>
Eh oui c'est tout : il s'agit bien de l'ensemble du scnario de test et c'est normal qu'il soit aussi court. Pas mal de choses se sont passes...
<ol>
<li>
La ncessit de crer le fichier uniquement si ncessaire a t dplace vers le <code>FileWriter</code>.
</li>
<li>
tant donn que nous travaillons avec des objets fantaisie, aucun fichier n'a t cr et donc <code>setUp()</code> et <code>tearDown()</code> passent dans les tests du scripteur.
</li>
<li>
Dsormais le test consiste simplement dans l'envoi d'un message type et du test de son format.
</li>
</ol>
Attendez un instant, o sont les assertions ?
</p>
<p>
Les objets fantaisie font beaucoup plus que se comporter comme des objets, ils excutent aussi des test. L'appel <code>expectArguments()</code> dit l'objet fantaisie d'attendre un seul paramtre de la chane "[Timestamp] Test" quand la mthode fantaise <code>write()</code> est appele. Lorsque cette mthode est appele les paramtres attendus sont compars avec ceci et un succs ou un chec est renvoy comme rsultat au test unitaire. C'est pourquoi un nouvel objet fantaisie a une rfrence vers <code>$this</code> dans son constructeur, il a besoin de ce <code>$this</code> pour l'envoi de son propre rsultat.
</p>
<p>
L'autre attente, c'est que le <code>write</code> ne soit appel qu'une seule et unique fois. Juste l'initialiser ne serait pas suffisant. L'objet fantaisie attendrait une ternit si la mthode n'tait jamais appele et par consquent n'enverrait jamais le message d'erreur la fin du test. Pour y faire face, l'appel <code>tally()</code> lui dit de vrifier le nombre d'appel ce moment l. Nous pouvons voir tout a en lanant les tests...
<div class="demo">
<h1>All tests</h1>
<span class="pass">Pass</span>: log_test.php->Log class test->testwriting->Arguments for [write] were [String: [Timestamp] Test line]<br />
<span class="pass">Pass</span>: log_test.php->Log class test->testwriting->Expected call count for [write] was [1], but got [1]<br />
<span class="pass">Pass</span>: clock_test.php->Clock class test->testclockadvance->Advancement<br />
<span class="pass">Pass</span>: clock_test.php->Clock class test->testclocktellstime->Now is the right time<br />
<div style="padding: 8px; margin-top: 1em; background-color: green; color: white;">3/3 test cases complete.
<strong>4</strong> passes and <strong>0</strong> fails.</div>
</div>
</p>
<p>
En fait nous pouvons encore raccourcir nos tests. L'attente de l'objet fantaisie <code>expectOnce()</code> peut combiner les deux attentes spares.
<php><![CDATA[
function testWriting() {
$clock = &new MockClock($this);
$clock->setReturnValue('now', 'Timestamp');
$writer = &new MockFileWriter($this);<strong>
$writer->expectOnce('write', array('[Timestamp] Test line'));</strong>
$log = &new Log($writer);
$log->message('Test line', &$clock);
$writer->tally();
}
]]></php>
Cela peut tre une abrviation utile.
</p>
<p>
<a class="target" name="frontiere"><h2>Classes frontires</h2></a>
</p>
<p>
Quelque chose de trs agrable est arrive au loggueur en plus de devenir purement et simplement plus court.
</p>
<p>
Les seules choses dont il dpend sont maintenant des classes que nous avons crites nous-mme et qui dans les tests sont simules : donc aucune dpendance hormis notre propre code PHP. Pas de fichier crire ni de dclenchement via une horloge attendre. Cela veut dire que le scnario de test <em>log_test.php</em> va s'excuter aussi vite que le processeur le permet. Par contraste les classes <code>FileWriter</code> et <code>Clock</code> sont trs proches du systme. Plus difficile tester puisque de vraies donnes doivent tre dplaces et valides avec soin, souvent par des astuces ad hoc.
</p>
<p>
Notre dernire factorisation a beaucoup aid. Les classes aux frontires de l'application et du systme, celles qui sont difficiles tester, sont dsormais plus courtes tant donn que le code d'I/O a t loign encore plus de la logique applicative. Il existe des liens directs vers des oprations PHP : <code>FileWriter::write()</code> s'apparente l'quivalent PHP <code>fwrite()</code> avec le fichier ouvert pour l'ajout et <code>Clock::now()</code> s'apparente lui aussi un quivalent PHP <code>time()</code>. Primo le dbogage devient plus simple. Secundo ces classes devraient bouger moins souvent.
</p>
<p>
Si elles ne changent pas beaucoup alors il n'y a aucune raison pour continuer en excuter les tests. Cela veut dire que les tests pour les classes frontires peuvent tre dplaces vers leur propre suite de tests, laissant les autres tourner plein rgime. En fait c'est comme a que j'ai tendance travailler et les scnarios de test de <a href="simple_test.php">SimpleTest</a> lui-mme sont diviss de cette manire.
</p>
<p>
Peut-tre que a ne vous parat pas beaucoup avec un test unitaire et deux tests aux frontires, mais une application typique peut contenir vingt classes de frontire et deux cent classes d'application. Pour continuer leur excution toute vitesse, vous voudrez les tenir spares.
</p>
<p>
De plus, un bon dveloppement passe par des dcisions de tri entre les composants utiliser. Peut-tre, qui sait, tous ces simulacres pourront <a href="improving_design_tutorial.php">amliorer votre conception</a>.
</p>
</content>
<internal>
<link>
<a name="#variation">Variations</a> sur un log
</link>
<link>
Abstraire un niveau supplmentaire via une classe <a name="#scripteur">fantaisie d'un scripteur</a>
</link>
<link>
Sparer les tests des <a name="#frontiere">classes frontires</a> pour un petit nettoyage
</link>
</internal>
<external>
<link>
Ce tutorial suit l'introduction aux <a href="mock_objects_tutorial.php">objets fantaisies</a>.
</link>
<link>
Ensuite vient la <a href="improving_design_tutorial.php">conception pilote par les tests</a>.
</link>
<link>
Vous aurez besoin du <a href="simple_test.php">framework de test SimpleTest</a> pour essayer ces exemples.
</link>
</external>
<meta>
<keywords>
dveloppement logiciel,
programmation php,
outils de dveloppement logiciel,
tutorial php,
scripts php gratuits,
organisation de tests unitaires,
conseil de test,
astuce de dveloppement,
architecture logicielle pour des tests,
exemple de code php,
objets fantaisie,
port de junit,
exemples de scnarios de test,
test php,
outil de test unitaire,
suite de test php
</keywords>
</meta>
</page>
|