Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<html>
2
<head>
3
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
4
<title>SimpleTest for PHP partial mocks documentation</title>
5
<link rel="stylesheet" type="text/css" href="docs.css" title="Styles">
6
</head>
7
<body>
8
<div class="menu_back">
9
<div class="menu">
10
<h2>
11
<a href="index.html">SimpleTest</a>
12
</h2>
13
<ul>
14
<li>
15
<a href="overview.html">Overview</a>
16
</li>
17
<li>
18
<a href="unit_test_documentation.html">Unit tester</a>
19
</li>
20
<li>
21
<a href="group_test_documentation.html">Group tests</a>
22
</li>
23
<li>
24
<a href="mock_objects_documentation.html">Mock objects</a>
25
</li>
26
<li>
27
<span class="chosen">Partial mocks</span>
28
</li>
29
<li>
30
<a href="reporter_documentation.html">Reporting</a>
31
</li>
32
<li>
33
<a href="expectation_documentation.html">Expectations</a>
34
</li>
35
<li>
36
<a href="web_tester_documentation.html">Web tester</a>
37
</li>
38
<li>
39
<a href="form_testing_documentation.html">Testing forms</a>
40
</li>
41
<li>
42
<a href="authentication_documentation.html">Authentication</a>
43
</li>
44
<li>
45
<a href="browser_documentation.html">Scriptable browser</a>
46
</li>
47
</ul>
48
</div>
49
</div>
50
<h1>Partial mock objects documentation</h1>
51
<div class="content">
52
 
53
            <p>
54
                A partial mock is simply a pattern to alleviate a specific problem
55
                in testing with mock objects,
56
                that of getting mock objects into tight corners.
57
                It's quite a limited tool and possibly not even a good idea.
58
                It is included with SimpleTest because I have found it useful
59
                on more than one occasion and has saved a lot of work at that point.
60
            </p>
61
 
62
        <p>
63
<a class="target" name="inject">
64
<h2>The mock injection problem</h2>
65
</a>
66
</p>
67
            <p>
68
                When one object uses another it is very simple to just pass a mock
69
                version in already set up with its expectations.
70
                Things are rather tricker if one object creates another and the
71
                creator is the one you want to test.
72
                This means that the created object should be mocked, but we can
73
                hardly tell our class under test to create a mock instead.
74
                The tested class doesn't even know it is running inside a test
75
                after all.
76
            </p>
77
            <p>
78
                For example, suppose we are building a telnet client and it
79
                needs to create a network socket to pass its messages.
80
                The connection method might look something like...
81
<pre>
82
<strong>&lt;?php
83
    require_once('socket.php');
84
 
85
    class Telnet {
86
        ...
87
        function &amp;connect($ip, $port, $username, $password) {
88
            $socket = &amp;new Socket($ip, $port);
89
            $socket-&gt;read( ... );
90
            ...
91
        }
92
    }
93
?&gt;</strong>
94
</pre>
95
                We would really like to have a mock object version of the socket
96
                here, what can we do?
97
            </p>
98
            <p>
99
                The first solution is to pass the socket in as a parameter,
100
                forcing the creation up a level.
101
                Having the client handle this is actually a very good approach
102
                if you can manage it and should lead to factoring the creation from
103
                the doing.
104
                In fact, this is one way in which testing with mock objects actually
105
                forces you to code more tightly focused solutions.
106
                They improve your programming.
107
            </p>
108
            <p>
109
                Here this would be...
110
<pre>
111
&lt;?php
112
    require_once('socket.php');
113
 
114
    class Telnet {
115
        ...
116
        <strong>function &amp;connect(&amp;$socket, $username, $password) {
117
            $socket-&gt;read( ... );
118
            ...
119
        }</strong>
120
    }
121
?&gt;
122
</pre>
123
                This means that the test code is typical for a test involving
124
                mock objects.
125
<pre>
126
class TelnetTest extends UnitTestCase {
127
    ...
128
    function testConnection() {<strong>
129
        $socket = &amp;new MockSocket($this);
130
        ...
131
        $telnet = &amp;new Telnet();
132
        $telnet-&gt;connect($socket, 'Me', 'Secret');
133
        ...</strong>
134
    }
135
}
136
</pre>
137
                It is pretty obvious though that one level is all you can go.
138
                You would hardly want your top level application creating
139
                every low level file, socket and database connection ever
140
                needed.
141
                It wouldn't know the constructor parameters anyway.
142
            </p>
143
            <p>
144
                The next simplest compromise is to have the created object passed
145
                in as an optional parameter...
146
<pre>
147
&lt;?php
148
    require_once('socket.php');
149
 
150
    class Telnet {
151
        ...<strong>
152
        function &amp;connect($ip, $port, $username, $password, $socket = false) {
153
            if (!$socket) {
154
                $socket = &amp;new Socket($ip, $port);
155
            }
156
            $socket-&gt;read( ... );</strong>
157
            ...
158
            return $socket;
159
        }
160
    }
161
?&gt;
162
</pre>
163
                For a quick solution this is usually good enough.
164
                The test now looks almost the same as if the parameter
165
                was formally passed...
166
<pre>
167
class TelnetTest extends UnitTestCase {
168
    ...
169
    function testConnection() {<strong>
170
        $socket = &amp;new MockSocket($this);
171
        ...
172
        $telnet = &amp;new Telnet();
173
        $telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret', &amp;$socket);
174
        ...</strong>
175
    }
176
}
177
</pre>
178
                The problem with this approach is its untidiness.
179
                There is test code in the main class and parameters passed
180
                in the test case that are never used.
181
                This is a quick and dirty approach, but nevertheless effective
182
                in most situations.
183
            </p>
184
            <p>
185
                The next method is to pass in a factory object to do the creation...
186
<pre>
187
&lt;?php
188
    require_once('socket.php');
189
 
190
    class Telnet {<strong>
191
        function Telnet(&amp;$network) {
192
            $this-&gt;_network = &amp;$network;
193
        }</strong>
194
        ...
195
        function &amp;connect($ip, $port, $username, $password) {<strong>
196
            $socket = &amp;$this-&gt;_network-&gt;createSocket($ip, $port);
197
            $socket-&gt;read( ... );</strong>
198
            ...
199
            return $socket;
200
        }
201
    }
202
?&gt;
203
</pre>
204
                This is probably the most highly factored answer as creation
205
                is now moved into a small specialist class.
206
                The networking factory can now be tested separately, but mocked
207
                easily when we are testing the telnet class...
208
<pre>
209
class TelnetTest extends UnitTestCase {
210
    ...
211
    function testConnection() {<strong>
212
        $socket = &amp;new MockSocket($this);
213
        ...
214
        $network = &amp;new MockNetwork($this);
215
        $network-&gt;setReturnReference('createSocket', $socket);
216
        $telnet = &amp;new Telnet($network);
217
        $telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
218
        ...</strong>
219
    }
220
}
221
</pre>
222
                The downside is that we are adding a lot more classes to the
223
                library.
224
                Also we are passing a lot of factories around which will
225
                make the code a little less intuitive.
226
                The most flexible solution, but the most complex.
227
            </p>
228
            <p>
229
                Is there a middle ground?
230
            </p>
231
 
232
        <p>
233
<a class="target" name="creation">
234
<h2>Protected factory method</h2>
235
</a>
236
</p>
237
            <p>
238
                There is a way we can circumvent the problem without creating
239
                any new application classes, but it involves creating a subclass
240
                when we do the actual testing.
241
                Firstly we move the socket creation into its own method...
242
<pre>
243
&lt;?php
244
    require_once('socket.php');
245
 
246
    class Telnet {
247
        ...
248
        function &amp;connect($ip, $port, $username, $password) {<strong>
249
            $socket = &amp;$this-&gt;_createSocket($ip, $port);</strong>
250
            $socket-&gt;read( ... );
251
            ...
252
        }<strong>
253
 
254
        function &amp;_createSocket($ip, $port) {
255
            return new Socket($ip, $port);
256
        }</strong>
257
    }
258
?&gt;
259
</pre>
260
                This is the only change we make to the application code.
261
            </p>
262
            <p>
263
                For the test case we have to create a subclass so that
264
                we can intercept the socket creation...
265
<pre>
266
<strong>class TelnetTestVersion extends Telnet {
267
    var $_mock;
268
 
269
    function TelnetTestVersion(&amp;$mock) {
270
        $this-&gt;_mock = &amp;$mock;
271
        $this-&gt;Telnet();
272
    }
273
 
274
    function &amp;_createSocket() {
275
        return $this-&gt;_mock;
276
    }
277
}</strong>
278
</pre>
279
                Here I have passed the mock in the constructor, but a
280
                setter would have done just as well.
281
                Note that the mock was set into the object variable
282
                before the constructor was chained.
283
                This is necessary in case the constructor calls
284
                <span class="new_code">connect()</span>.
285
                Otherwise it could get a null value from
286
                <span class="new_code">_createSocket()</span>.
287
            </p>
288
            <p>
289
                After the completion of all of this extra work the
290
                actual test case is fairly easy.
291
                We just test our new class instead...
292
<pre>
293
class TelnetTest extends UnitTestCase {
294
    ...
295
    function testConnection() {<strong>
296
        $socket = &amp;new MockSocket($this);
297
        ...
298
        $telnet = &amp;new TelnetTestVersion($socket);
299
        $telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
300
        ...</strong>
301
    }
302
}
303
</pre>
304
                The new class is very simple of course.
305
                It just sets up a return value, rather like a mock.
306
                It would be nice if it also checked the incoming parameters
307
                as well.
308
                Just like a mock.
309
                It seems we are likely to do this often, can
310
                we automate the subclass creation?
311
            </p>
312
 
313
        <p>
314
<a class="target" name="partial">
315
<h2>A partial mock</h2>
316
</a>
317
</p>
318
            <p>
319
                Of course the answer is "yes" or I would have stopped writing
320
                this by now!
321
                The previous test case was a lot of work, but we can
322
                generate the subclass using a similar approach to the mock objects.
323
            </p>
324
            <p>
325
                Here is the partial mock version of the test...
326
<pre>
327
<strong>Mock::generatePartial(
328
        'Telnet',
329
        'TelnetTestVersion',
330
        array('_createSocket'));</strong>
331
 
332
class TelnetTest extends UnitTestCase {
333
    ...
334
    function testConnection() {<strong>
335
        $socket = &amp;new MockSocket($this);
336
        ...
337
        $telnet = &amp;new TelnetTestVersion($this);
338
        $telnet-&gt;setReturnReference('_createSocket', $socket);
339
        $telnet-&gt;Telnet();
340
        $telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
341
        ...</strong>
342
    }
343
}
344
</pre>
345
                The partial mock is a subclass of the original with
346
                selected methods "knocked out" with test
347
                versions.
348
                The <span class="new_code">generatePartial()</span> call
349
                takes three parameters: the class to be subclassed,
350
                the new test class name and a list of methods to mock.
351
            </p>
352
            <p>
353
                Instantiating the resulting objects is slightly tricky.
354
                The only constructor parameter of a partial mock is
355
                the unit tester reference.
356
                As with the normal mock objects this is needed for sending
357
                test results in response to checked expectations.
358
            </p>
359
            <p>
360
                The original constructor is not run yet.
361
                This is necessary in case the constructor is going to
362
                make use of the as yet unset mocked methods.
363
                We set any return values at this point and then run the
364
                constructor with its normal parameters.
365
                This three step construction of "new", followed
366
                by setting up the methods, followed by running the constructor
367
                proper is what distinguishes the partial mock code.
368
            </p>
369
            <p>
370
                Apart from construction, all of the mocked methods have
371
                the same features as mock objects and all of the unmocked
372
                methods behave as before.
373
                We can set expectations very easily...
374
<pre>
375
class TelnetTest extends UnitTestCase {
376
    ...
377
    function testConnection() {
378
        $socket = &amp;new MockSocket($this);
379
        ...
380
        $telnet = &amp;new TelnetTestVersion($this);
381
        $telnet-&gt;setReturnReference('_createSocket', $socket);<strong>
382
        $telnet-&gt;expectOnce('_createSocket', array('127.0.0.1', 21));</strong>
383
        $telnet-&gt;Telnet();
384
        $telnet-&gt;connect('127.0.0.1', 21, 'Me', 'Secret');
385
        ...<strong>
386
        $telnet-&gt;tally();</strong>
387
    }
388
}
389
</pre>
390
            </p>
391
 
392
        <p>
393
<a class="target" name="less">
394
<h2>Testing less than a class</h2>
395
</a>
396
</p>
397
            <p>
398
                The mocked out methods don't have to be factory methods,
399
                they could be any sort of method.
400
                In this way partial mocks allow us to take control of any part of
401
                a class except the constructor.
402
                We could even go as far as to mock every method
403
                except one we actually want to test.
404
            </p>
405
            <p>
406
                This last situation is all rather hypothetical, as I haven't
407
                tried it.
408
                I am open to the possibility, but a little worried that
409
                forcing object granularity may be better for the code quality.
410
                I personally use partial mocks as a way of overriding creation
411
                or for occasional testing of the TemplateMethod pattern.
412
            </p>
413
            <p>
414
                It's all going to come down to the coding standards of your
415
                project to decide which mechanism you use.
416
            </p>
417
 
418
    </div>
419
<div class="copyright">
420
            Copyright<br>Marcus Baker, Jason Sweat, Perrick Penet 2004
421
        </div>
422
</body>
423
</html>