- 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. -
- -