File: boundary_classes_tutorial.xml

package info (click to toggle)
postfixadmin 2.3.5-2%2Bdeb7u1
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 6,200 kB
  • sloc: php: 25,767; xml: 14,485; perl: 964; sh: 664; python: 169; makefile: 84
file content (268 lines) | stat: -rw-r--r-- 16,941 bytes parent folder | download | duplicates (2)
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 &quot;cette chose vers laquelle on crit&quot; 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 &quot;[Timestamp] Test&quot; 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-&gt;Log class test-&gt;testwriting-&gt;Arguments for [write] were [String: [Timestamp] Test line]<br />
                <span class="pass">Pass</span>: log_test.php-&gt;Log class test-&gt;testwriting-&gt;Expected call count for [write] was [1], but got [1]<br />
                
                <span class="pass">Pass</span>: clock_test.php-&gt;Clock class test-&gt;testclockadvance-&gt;Advancement<br />
                <span class="pass">Pass</span>: clock_test.php-&gt;Clock class test-&gt;testclocktellstime-&gt;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>