Blame | Letzte Änderung | Log anzeigen | RSS feed
<html><head><META http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>SimpleTest for PHP partial mocks documentation</title><link rel="stylesheet" type="text/css" href="docs.css" title="Styles"></head><body><div class="menu_back"><div class="menu"><h2><a href="index.html">SimpleTest</a></h2><ul><li><a href="overview.html">Overview</a></li><li><a href="unit_test_documentation.html">Unit tester</a></li><li><a href="group_test_documentation.html">Group tests</a></li><li><a href="mock_objects_documentation.html">Mock objects</a></li><li><span class="chosen">Partial mocks</span></li><li><a href="reporter_documentation.html">Reporting</a></li><li><a href="expectation_documentation.html">Expectations</a></li><li><a href="web_tester_documentation.html">Web tester</a></li><li><a href="form_testing_documentation.html">Testing forms</a></li><li><a href="authentication_documentation.html">Authentication</a></li><li><a href="browser_documentation.html">Scriptable browser</a></li></ul></div></div><h1>Partial mock objects documentation</h1><div class="content"><p>A partial mock is simply a pattern to alleviate a specific problemin testing with mock objects,that of getting mock objects into tight corners.It's quite a limited tool and possibly not even a good idea.It is included with SimpleTest because I have found it usefulon more than one occasion and has saved a lot of work at that point.</p><p><a class="target" name="inject"><h2>The mock injection problem</h2></a></p><p>When one object uses another it is very simple to just pass a mockversion in already set up with its expectations.Things are rather tricker if one object creates another and thecreator is the one you want to test.This means that the created object should be mocked, but we canhardly tell our class under test to create a mock instead.The tested class doesn't even know it is running inside a testafter all.</p><p>For example, suppose we are building a telnet client and itneeds to create a network socket to pass its messages.The connection method might look something like...<pre><strong><?phprequire_once('socket.php');class Telnet {...function &connect($ip, $port, $username, $password) {$socket = &new Socket($ip, $port);$socket->read( ... );...}}?></strong></pre>We would really like to have a mock object version of the sockethere, what can we do?</p><p>The first solution is to pass the socket in as a parameter,forcing the creation up a level.Having the client handle this is actually a very good approachif you can manage it and should lead to factoring the creation fromthe doing.In fact, this is one way in which testing with mock objects actuallyforces you to code more tightly focused solutions.They improve your programming.</p><p>Here this would be...<pre><?phprequire_once('socket.php');class Telnet {...<strong>function &connect(&$socket, $username, $password) {$socket->read( ... );...}</strong>}?></pre>This means that the test code is typical for a test involvingmock objects.<pre>class TelnetTest extends UnitTestCase {...function testConnection() {<strong>$socket = &new MockSocket($this);...$telnet = &new Telnet();$telnet->connect($socket, 'Me', 'Secret');...</strong>}}</pre>It is pretty obvious though that one level is all you can go.You would hardly want your top level application creatingevery low level file, socket and database connection everneeded.It wouldn't know the constructor parameters anyway.</p><p>The next simplest compromise is to have the created object passedin as an optional parameter...<pre><?phprequire_once('socket.php');class Telnet {...<strong>function &connect($ip, $port, $username, $password, $socket = false) {if (!$socket) {$socket = &new Socket($ip, $port);}$socket->read( ... );</strong>...return $socket;}}?></pre>For a quick solution this is usually good enough.The test now looks almost the same as if the parameterwas formally passed...<pre>class TelnetTest extends UnitTestCase {...function testConnection() {<strong>$socket = &new MockSocket($this);...$telnet = &new Telnet();$telnet->connect('127.0.0.1', 21, 'Me', 'Secret', &$socket);...</strong>}}</pre>The problem with this approach is its untidiness.There is test code in the main class and parameters passedin the test case that are never used.This is a quick and dirty approach, but nevertheless effectivein most situations.</p><p>The next method is to pass in a factory object to do the creation...<pre><?phprequire_once('socket.php');class Telnet {<strong>function Telnet(&$network) {$this->_network = &$network;}</strong>...function &connect($ip, $port, $username, $password) {<strong>$socket = &$this->_network->createSocket($ip, $port);$socket->read( ... );</strong>...return $socket;}}?></pre>This is probably the most highly factored answer as creationis now moved into a small specialist class.The networking factory can now be tested separately, but mockedeasily when we are testing the telnet class...<pre>class TelnetTest extends UnitTestCase {...function testConnection() {<strong>$socket = &new MockSocket($this);...$network = &new MockNetwork($this);$network->setReturnReference('createSocket', $socket);$telnet = &new Telnet($network);$telnet->connect('127.0.0.1', 21, 'Me', 'Secret');...</strong>}}</pre>The downside is that we are adding a lot more classes to thelibrary.Also we are passing a lot of factories around which willmake the code a little less intuitive.The most flexible solution, but the most complex.</p><p>Is there a middle ground?</p><p><a class="target" name="creation"><h2>Protected factory method</h2></a></p><p>There is a way we can circumvent the problem without creatingany new application classes, but it involves creating a subclasswhen we do the actual testing.Firstly we move the socket creation into its own method...<pre><?phprequire_once('socket.php');class Telnet {...function &connect($ip, $port, $username, $password) {<strong>$socket = &$this->_createSocket($ip, $port);</strong>$socket->read( ... );...}<strong>function &_createSocket($ip, $port) {return new Socket($ip, $port);}</strong>}?></pre>This is the only change we make to the application code.</p><p>For the test case we have to create a subclass so thatwe can intercept the socket creation...<pre><strong>class TelnetTestVersion extends Telnet {var $_mock;function TelnetTestVersion(&$mock) {$this->_mock = &$mock;$this->Telnet();}function &_createSocket() {return $this->_mock;}}</strong></pre>Here I have passed the mock in the constructor, but asetter would have done just as well.Note that the mock was set into the object variablebefore the constructor was chained.This is necessary in case the constructor calls<span class="new_code">connect()</span>.Otherwise it could get a null value from<span class="new_code">_createSocket()</span>.</p><p>After the completion of all of this extra work theactual test case is fairly easy.We just test our new class instead...<pre>class TelnetTest extends UnitTestCase {...function testConnection() {<strong>$socket = &new MockSocket($this);...$telnet = &new TelnetTestVersion($socket);$telnet->connect('127.0.0.1', 21, 'Me', 'Secret');...</strong>}}</pre>The new class is very simple of course.It just sets up a return value, rather like a mock.It would be nice if it also checked the incoming parametersas well.Just like a mock.It seems we are likely to do this often, canwe automate the subclass creation?</p><p><a class="target" name="partial"><h2>A partial mock</h2></a></p><p>Of course the answer is "yes" or I would have stopped writingthis by now!The previous test case was a lot of work, but we cangenerate the subclass using a similar approach to the mock objects.</p><p>Here is the partial mock version of the test...<pre><strong>Mock::generatePartial('Telnet','TelnetTestVersion',array('_createSocket'));</strong>class TelnetTest extends UnitTestCase {...function testConnection() {<strong>$socket = &new MockSocket($this);...$telnet = &new TelnetTestVersion($this);$telnet->setReturnReference('_createSocket', $socket);$telnet->Telnet();$telnet->connect('127.0.0.1', 21, 'Me', 'Secret');...</strong>}}</pre>The partial mock is a subclass of the original withselected methods "knocked out" with testversions.The <span class="new_code">generatePartial()</span> calltakes three parameters: the class to be subclassed,the new test class name and a list of methods to mock.</p><p>Instantiating the resulting objects is slightly tricky.The only constructor parameter of a partial mock isthe unit tester reference.As with the normal mock objects this is needed for sendingtest results in response to checked expectations.</p><p>The original constructor is not run yet.This is necessary in case the constructor is going tomake use of the as yet unset mocked methods.We set any return values at this point and then run theconstructor with its normal parameters.This three step construction of "new", followedby setting up the methods, followed by running the constructorproper is what distinguishes the partial mock code.</p><p>Apart from construction, all of the mocked methods havethe same features as mock objects and all of the unmockedmethods behave as before.We can set expectations very easily...<pre>class TelnetTest extends UnitTestCase {...function testConnection() {$socket = &new MockSocket($this);...$telnet = &new TelnetTestVersion($this);$telnet->setReturnReference('_createSocket', $socket);<strong>$telnet->expectOnce('_createSocket', array('127.0.0.1', 21));</strong>$telnet->Telnet();$telnet->connect('127.0.0.1', 21, 'Me', 'Secret');...<strong>$telnet->tally();</strong>}}</pre></p><p><a class="target" name="less"><h2>Testing less than a class</h2></a></p><p>The mocked out methods don't have to be factory methods,they could be any sort of method.In this way partial mocks allow us to take control of any part ofa class except the constructor.We could even go as far as to mock every methodexcept one we actually want to test.</p><p>This last situation is all rather hypothetical, as I haven'ttried it.I am open to the possibility, but a little worried thatforcing object granularity may be better for the code quality.I personally use partial mocks as a way of overriding creationor for occasional testing of the TemplateMethod pattern.</p><p>It's all going to come down to the coding standards of yourproject to decide which mechanism you use.</p></div><div class="copyright">Copyright<br>Marcus Baker, Jason Sweat, Perrick Penet 2004</div></body></html>