- A partial mock is simply a pattern to alleviate a specific problem - in 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 useful - on more than one occasion and has saved a lot of work at that point. -
- -
-
-The mock injection problem
-
-
- When one object uses another it is very simple to just pass a mock - version in already set up with its expectations. - Things are rather tricker if one object creates another and the - creator is the one you want to test. - This means that the created object should be mocked, but we can - hardly tell our class under test to create a mock instead. - The tested class doesn't even know it is running inside a test - after all. -
-- For example, suppose we are building a telnet client and it - needs to create a network socket to pass its messages. - The connection method might look something like... -
-<?php - require_once('socket.php'); - - class Telnet { - ... - function &connect($ip, $port, $username, $password) { - $socket = &new Socket($ip, $port); - $socket->read( ... ); - ... - } - } -?> -- We would really like to have a mock object version of the socket - here, what can we do? - -
- 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 approach - if you can manage it and should lead to factoring the creation from - the doing. - In fact, this is one way in which testing with mock objects actually - forces you to code more tightly focused solutions. - They improve your programming. -
-- Here this would be... -
-<?php - require_once('socket.php'); - - class Telnet { - ... - function &connect(&$socket, $username, $password) { - $socket->read( ... ); - ... - } - } -?> -- This means that the test code is typical for a test involving - mock objects. -
-class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $socket = &new MockSocket($this); - ... - $telnet = &new Telnet(); - $telnet->connect($socket, 'Me', 'Secret'); - ... - } -} -- It is pretty obvious though that one level is all you can go. - You would hardly want your top level application creating - every low level file, socket and database connection ever - needed. - It wouldn't know the constructor parameters anyway. - -
- The next simplest compromise is to have the created object passed - in as an optional parameter... -
-<?php - require_once('socket.php'); - - class Telnet { - ... - function &connect($ip, $port, $username, $password, $socket = false) { - if (!$socket) { - $socket = &new Socket($ip, $port); - } - $socket->read( ... ); - ... - return $socket; - } - } -?> -- For a quick solution this is usually good enough. - The test now looks almost the same as if the parameter - was formally passed... -
-class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $socket = &new MockSocket($this); - ... - $telnet = &new Telnet(); - $telnet->connect('127.0.0.1', 21, 'Me', 'Secret', &$socket); - ... - } -} -- The problem with this approach is its untidiness. - There is test code in the main class and parameters passed - in the test case that are never used. - This is a quick and dirty approach, but nevertheless effective - in most situations. - -
- The next method is to pass in a factory object to do the creation... -
-<?php - require_once('socket.php'); - - class Telnet { - function Telnet(&$network) { - $this->_network = &$network; - } - ... - function &connect($ip, $port, $username, $password) { - $socket = &$this->_network->createSocket($ip, $port); - $socket->read( ... ); - ... - return $socket; - } - } -?> -- This is probably the most highly factored answer as creation - is now moved into a small specialist class. - The networking factory can now be tested separately, but mocked - easily when we are testing the telnet class... -
-class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $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'); - ... - } -} -- The downside is that we are adding a lot more classes to the - library. - Also we are passing a lot of factories around which will - make the code a little less intuitive. - The most flexible solution, but the most complex. - -
- Is there a middle ground? -
- -
-
-Protected factory method
-
-
- There is a way we can circumvent the problem without creating - any new application classes, but it involves creating a subclass - when we do the actual testing. - Firstly we move the socket creation into its own method... -
-<?php - require_once('socket.php'); - - class Telnet { - ... - function &connect($ip, $port, $username, $password) { - $socket = &$this->_createSocket($ip, $port); - $socket->read( ... ); - ... - } - - function &_createSocket($ip, $port) { - return new Socket($ip, $port); - } - } -?> -- This is the only change we make to the application code. - -
- For the test case we have to create a subclass so that - we can intercept the socket creation... -
-class TelnetTestVersion extends Telnet { - var $_mock; - - function TelnetTestVersion(&$mock) { - $this->_mock = &$mock; - $this->Telnet(); - } - - function &_createSocket() { - return $this->_mock; - } -} -- Here I have passed the mock in the constructor, but a - setter would have done just as well. - Note that the mock was set into the object variable - before the constructor was chained. - This is necessary in case the constructor calls - connect(). - Otherwise it could get a null value from - _createSocket(). - -
- After the completion of all of this extra work the - actual test case is fairly easy. - We just test our new class instead... -
-class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $socket = &new MockSocket($this); - ... - $telnet = &new TelnetTestVersion($socket); - $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); - ... - } -} -- 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 parameters - as well. - Just like a mock. - It seems we are likely to do this often, can - we automate the subclass creation? - - - -
- Of course the answer is "yes" or I would have stopped writing - this by now! - The previous test case was a lot of work, but we can - generate the subclass using a similar approach to the mock objects. -
-- Here is the partial mock version of the test... -
-Mock::generatePartial( - 'Telnet', - 'TelnetTestVersion', - array('_createSocket')); - -class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $socket = &new MockSocket($this); - ... - $telnet = &new TelnetTestVersion($this); - $telnet->setReturnReference('_createSocket', $socket); - $telnet->Telnet(); - $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); - ... - } -} -- The partial mock is a subclass of the original with - selected methods "knocked out" with test - versions. - The generatePartial() call - takes three parameters: the class to be subclassed, - the new test class name and a list of methods to mock. - -
- Instantiating the resulting objects is slightly tricky. - The only constructor parameter of a partial mock is - the unit tester reference. - As with the normal mock objects this is needed for sending - test results in response to checked expectations. -
-- The original constructor is not run yet. - This is necessary in case the constructor is going to - make use of the as yet unset mocked methods. - We set any return values at this point and then run the - constructor with its normal parameters. - This three step construction of "new", followed - by setting up the methods, followed by running the constructor - proper is what distinguishes the partial mock code. -
-- Apart from construction, all of the mocked methods have - the same features as mock objects and all of the unmocked - methods behave as before. - We can set expectations very easily... -
-class TelnetTest extends UnitTestCase { - ... - function testConnection() { - $socket = &new MockSocket($this); - ... - $telnet = &new TelnetTestVersion($this); - $telnet->setReturnReference('_createSocket', $socket); - $telnet->expectOnce('_createSocket', array('127.0.0.1', 21)); - $telnet->Telnet(); - $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); - ... - $telnet->tally(); - } -} -- - -
-
-Testing less than a class
-
-
- 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 of - a class except the constructor. - We could even go as far as to mock every method - except one we actually want to test. -
-- This last situation is all rather hypothetical, as I haven't - tried it. - I am open to the possibility, but a little worried that - forcing object granularity may be better for the code quality. - I personally use partial mocks as a way of overriding creation - or for occasional testing of the TemplateMethod pattern. -
-- It's all going to come down to the coding standards of your - project to decide which mechanism you use. -
- -