+ Mock objects have two roles during a test case: actor and critic. +
++ The actor behaviour is to simulate objects that are difficult to + set up or time consuming to set up for a test. + The classic example is a database connection. + Setting up a test database at the start of each test would slow + testing to a crawl and would require the installation of the + database engine and test data on the test machine. + If we can simulate the connection and return data of our + choosing we not only win on the pragmatics of testing, but can + also feed our code spurious data to see how it responds. + We can simulate databases being down or other extremes + without having to create a broken database for real. + In other words, we get greater control of the test environment. +
++ If mock objects only behaved as actors they would simply be + known as server stubs. +
++ However, the mock objects not only play a part (by supplying chosen + return values on demand) they are also sensitive to the + messages sent to them (via expectations). + By setting expected parameters for a method call they act + as a guard that the calls upon them are made correctly. + If expectations are not met they save us the effort of + writing a failed test assertion by performing that duty on our + behalf. + In the case of an imaginary database connection they can + test that the query, say SQL, was correctly formed by + the object that is using the connection. + Set them up with fairly tight expectations and you will + hardly need manual assertions at all. +
+ + ++ In the same way that we create server stubs, all we need is an + existing class, say a database connection that looks like this... +
+class DatabaseConnection {
+ function DatabaseConnection() {
+ }
+
+ function query() {
+ }
+
+ function selectQuery() {
+ }
+}
+
+ The class does not need to have been implemented yet.
+ To create a mock version of the class we need to include the
+ mock object library and run the generator...
+
+require_once('simpletest/unit_tester.php');
+require_once('simpletest/mock_objects.php');
+require_once('database_connection.php');
+
+Mock::generate('DatabaseConnection');
+
+ This generates a clone class called
+ MockDatabaseConnection.
+ We can now create instances of the new class within
+ our test case...
+
+require_once('simpletest/unit_tester.php');
+require_once('simpletest/mock_objects.php');
+require_once('database_connection.php');
+
+Mock::generate('DatabaseConnection');
+
+class MyTestCase extends UnitTestCase {
+
+ function testSomething() {
+ $connection = &new MockDatabaseConnection($this);
+ }
+}
+
+ Unlike the generated stubs the mock constructor needs a reference
+ to the test case so that it can dispatch passes and failures while
+ checking its expectations.
+ This means that mock objects can only be used within test cases.
+ Despite this their extra power means that stubs are hardly ever used
+ if mocks are available.
+
+
+ + The mock version of a class has all the methods of the original + so that operations like + $connection->query() are still + legal. + As with stubs we can replace the default null return values... +
+$connection->setReturnValue('query', 37);
+
+ Now every time we call
+ $connection->query() we get
+ the result of 37.
+ As with the stubs we can set wildcards and we can overload the
+ wildcard parameter.
+ We can also add extra methods to the mock when generating it
+ and choose our own class name...
+
+Mock::generate('DatabaseConnection', 'MyMockDatabaseConnection', array('setOptions'));
+
+ Here the mock will behave as if the setOptions()
+ existed in the original class.
+ This is handy if a class has used the PHP overload()
+ mechanism to add dynamic methods.
+ You can create a special mock to simulate this situation.
+
+ + All of the patterns available with server stubs are available + to mock objects... +
+class Iterator {
+ function Iterator() {
+ }
+
+ function next() {
+ }
+}
+
+ Again, assuming that this iterator only returns text until it
+ reaches the end, when it returns false, we can simulate it
+ with...
+
+Mock::generate('Iterator');
+
+class IteratorTest extends UnitTestCase() {
+
+ function testASequence() {
+ $iterator = &new MockIterator($this);
+ $iterator->setReturnValue('next', false);
+ $iterator->setReturnValueAt(0, 'next', 'First string');
+ $iterator->setReturnValueAt(1, 'next', 'Second string');
+ ...
+ }
+}
+
+ When next() is called on the
+ mock iterator it will first return "First string",
+ on the second call "Second string" will be returned
+ and on any other call false will
+ be returned.
+ The sequenced return values take precedence over the constant
+ return value.
+ The constant one is a kind of default if you like.
+
+ + A repeat of the stubbed information holder with name/value pairs... +
+class Configuration {
+ function Configuration() {
+ }
+
+ function getValue($key) {
+ }
+}
+
+ This is a classic situation for using mock objects as
+ actual configuration will vary from machine to machine,
+ hardly helping the reliability of our tests if we use it
+ directly.
+ The problem though is that all the data comes through the
+ getValue() method and yet
+ we want different results for different keys.
+ Luckily the mocks have a filter system...
+
+$config = &new MockConfiguration($this);
+$config->setReturnValue('getValue', 'primary', array('db_host'));
+$config->setReturnValue('getValue', 'admin', array('db_user'));
+$config->setReturnValue('getValue', 'secret', array('db_password'));
+
+ The extra parameter is a list of arguments to attempt
+ to match.
+ In this case we are trying to match only one argument which
+ is the look up key.
+ Now when the mock object has the
+ getValue() method invoked
+ like this...
+
+$config->getValue('db_user')
+
+ ...it will return "admin".
+ It finds this by attempting to match the calling arguments
+ to its list of returns one after another until
+ a complete match is found.
+
+ + There are times when you want a specific object to be + dished out by the mock rather than a copy. + Again this is identical to the server stubs mechanism... +
+class Thing {
+}
+
+class Vector {
+ function Vector() {
+ }
+
+ function get($index) {
+ }
+}
+
+ In this case you can set a reference into the mock's
+ return list...
+
+$thing = new Thing();
+$vector = &new MockVector($this);
+$vector->setReturnReference('get', $thing, array(12));
+
+ With this arrangement you know that every time
+ $vector->get(12) is
+ called it will return the same
+ $thing each time.
+
+
+
+ + Although the server stubs approach insulates your tests from + real world disruption, it is only half the benefit. + You can have the class under test receiving the required + messages, but is your new class sending correct ones? + Testing this can get messy without a mock objects library. +
++ By way of example, suppose we have a + SessionPool class that we + want to add logging to. + Rather than grow the original class into something more + complicated, we want to add this behaviour with a decorator (GOF). + The SessionPool code currently looks + like this... +
+class SessionPool {
+ function SessionPool() {
+ ...
+ }
+
+ function &findSession($cookie) {
+ ...
+ }
+ ...
+}
+
+class Session {
+ ...
+}
+</php>
+ While our logging code looks like this...
+<php>
+class Log {
+ function Log() {
+ ...
+ }
+
+ function message() {
+ ...
+ }
+}
+
+class LoggingSessionPool {
+ function LoggingSessionPool(&$session_pool, &$log) {
+ ...
+ }
+
+ function &findSession(\$cookie) {
+ ...
+ }
+ ...
+}
+
+ Out of all of this, the only class we want to test here
+ is the LoggingSessionPool.
+ In particular we would like to check that the
+ findSession() method is
+ called with the correct session ID in the cookie and that
+ it sent the message "Starting session $cookie"
+ to the logger.
+
+ + Despite the fact that we are testing only a few lines of + production code, here is what we would have to do in a + conventional test case: +
-
+
- Create a log object. +
- Set a directory to place the log file. +
- Set the directory permissions so we can write the log. +
- Create a SessionPool object. +
- Hand start a session, which probably does lot's of things. +
- Invoke findSession(). +
- Read the new Session ID (hope there is an accessor!). +
- Raise a test assertion to confirm that the ID matches the cookie. +
- Read the last line of the log file. +
- Pattern match out the extra logging timestamps, etc. +
- Assert that the session message is contained in the text. +
+ Instead, here is the complete test method using mock object magic... +
+Mock::generate('Session');
+Mock::generate('SessionPool');
+Mock::generate('Log');
+
+class LoggingSessionPoolTest extends UnitTestCase {
+ ...
+ function testFindSessionLogging() {
+ $session = &new MockSession($this);
+ $pool = &new MockSessionPool($this);
+ $pool->setReturnReference('findSession', $session);
+ $pool->expectOnce('findSession', array('abc'));
+
+ $log = &new MockLog($this);
+ $log->expectOnce('message', array('Starting session abc'));
+
+ $logging_pool = &new LoggingSessionPool($pool, $log);
+ $this->assertReference($logging_pool->findSession('abc'), $session);
+ $pool->tally();
+ $log->tally();
+ }
+}
+
+ We start by creating a dummy session.
+ We don't have to be too fussy about this as the check
+ for which session we want is done elsewhere.
+ We only need to check that it was the same one that came
+ from the session pool.
+
+ + findSession() is a factory + method the simulation of which is described above. + The point of departure comes with the first + expectOnce() call. + This line states that whenever + findSession() is invoked on the + mock, it will test the incoming arguments. + If it receives the single argument of a string "abc" + then a test pass is sent to the unit tester, otherwise a fail is + generated. + This was the part where we checked that the right session was asked for. + The argument list follows the same format as the one for setting + return values. + You can have wildcards and sequences and the order of + evaluation is the same. +
++ If the call is never made then neither a pass nor a failure will + generated. + To get around this we must tell the mock when the test is over + so that the object can decide if the expectation has been met. + The unit tester assertion for this is triggered by the + tally() call at the end of + the test. +
++ We use the same pattern to set up the mock logger. + We tell it that it should have + message() invoked + once only with the argument "Starting session abc". + By testing the calling arguments, rather than the logger output, + we insulate the test from any display changes in the logger. +
++ We start to run our tests when we create the new + LoggingSessionPool and feed + it our preset mock objects. + Everything is now under our control. + Finally we confirm that the + $session we gave our decorator + is the one that we get back and tell the mocks to run their + internal call count tests with the + tally() calls. +
++ This is still quite a bit of test code, but the code is very + strict. + If it still seems rather daunting there is a lot less of it + than if we tried this without mocks and this particular test, + interactions rather than output, is always more work to set + up. + More often you will be testing more complex situations without + needing this level or precision. + Also some of this can be refactored into a test case + setUp() method. +
++ Here is the full list of expectations you can set on a mock object + in SimpleTest... +
| Expectation | Needs tally() | +
|---|---|
| expectArguments($method, $args) | +No | +
| expectArgumentsAt($timing, $method, $args) | +No | +
| expectCallCount($method, $count) | +Yes | +
| expectMaximumCallCount($method, $count) | +No | +
| expectMinimumCallCount($method, $count) | +Yes | +
| expectNever($method) | +No | +
| expectOnce($method, $args) | +Yes | +
| expectAtLeastOnce($method, $args) | +Yes | +
-
+
- $method +
- The method name, as a string, to apply the condition to. +
- $args +
- + The arguments as a list. Wildcards can be included in the same + manner as for setReturn(). + This argument is optional for expectOnce() + and expectAtLeastOnce(). + +
- $timing +
- + The only point in time to test the condition. + The first call starts at zero. + +
- $count +
- The number of calls expected. +
+ Like the assertions within test cases, all of the expectations + can take a message override as an extra parameter. + Also the original failure message can be embedded in the output + as "%s". +
+ + ++ There are three approaches to creating mocks including the one + that SimpleTest employs. + Coding them by hand using a base class, generating them to + a file and dynamically generating them on the fly. +
++ Mock objects generated with SimpleTest + are dynamic. + They are created at run time in memory, using + eval(), rather than written + out to a file. + This makes the mocks easy to create, a one liner, + especially compared with hand + crafting them in a parallel class hierarchy. + The problem is that the behaviour is usually set up in the tests + themselves. + If the original objects change the mock versions + that the tests rely on can get out of sync. + This can happen with the parallel hierarchy approach as well, + but is far more quickly detected. +
++ The solution, of course, is to add some real integration + tests. + You don't need very many and the convenience gained + from the mocks more than outweighs the small amount of + extra testing. + You cannot trust code that was only tested with mocks. +
++ If you are still determined to build static libraries of mocks + because you want to simulate very specific behaviour, you can + achieve the same effect using the SimpleTest class generator. + In your library file, say mocks/connection.php for a + database connection, create a mock and inherit to override + special methods or add presets... +
+<?php
+ require_once('simpletest/mock_objects.php');
+ require_once('../classes/connection.php');
+
+ Mock::generate('Connection', 'BasicMockConnection');
+ class MockConnection extends BasicMockConnection {
+ function MockConnection(&$test, $wildcard = '*') {
+ $this->BasicMockConnection($test, $wildcard);
+ $this->setReturn('query', false);
+ }
+ }
+?>
+
+ The generate call tells the class generator to create
+ a class called BasicMockConnection
+ rather than the usual MockConnection.
+ We then inherit from this to get our version of
+ MockConnection.
+ By intercepting in this way we can add behaviour, here setting
+ the default value of query() to be false.
+ By using the default name we make sure that the mock class
+ generator will not recreate a different one when invoked elsewhere in the
+ tests.
+ It never creates a class if it already exists.
+ As long as the above file is included first then all tests
+ that generated MockConnection should
+ now be using our one instead.
+ If we don't get the order right and the mock library
+ creates one first then the class creation will simply fail.
+
+ + Use this trick if you find you have a lot of common mock behaviour + or you are getting frequent integration problems at later + stages of testing. +
+ +
+
+I think SimpleTest stinks!
+
+
+ But at the time of writing it is the only one with mock objects, + so are you stuck with it? +
++ No, not at all. + SimpleTest is a toolkit and one of those + tools is the mock objects which can be employed independently. + Suppose you have your own favourite unit tester and all your current + test cases are written using it. + Pretend that you have called your unit tester PHPUnit (everyone else has) + and the core test class looks like this... +
+class PHPUnit {
+ function PHPUnit() {
+ }
+
+ function assertion($message, $assertion) {
+ }
+ ...
+}
+
+ All the assertion() method does
+ is print some fancy output and the boolean assertion parameter determines
+ whether to print a pass or a failure.
+ Let's say that it is used like this...
+
+$unit_test = new PHPUnit();
+$unit_test>assertion('I hope this file exists', file_exists('my_file'));
+
+ How do you use mocks with this?
+
+ + There is a protected method on the base mock class + SimpleMock called + _assertTrue() and + by overriding this method we can use our own assertion format. + We start with a subclass, in say my_mock.php... +
+<?php
+ require_once('simpletest/mock_objects.php');
+
+ class MyMock extends SimpleMock() {
+ function MyMock(&$test, $wildcard) {
+ $this->SimpleMock($test, $wildcard);
+ }
+
+ function _assertTrue($assertion, $message) {
+ $test = &$this->getTest();
+ $test->assertion($message, $assertion);
+ }
+ }
+?>
+
+ Now instantiating MyMock will create
+ an object that speaks the same language as your tester.
+ The catch is of course that we never create such an object, the
+ code generator does.
+ We need just one more line of code to tell the generator to use
+ your mock instead...
+
+<?php
+ require_once('simpletst/mock_objects.php');
+
+ class MyMock extends SimpleMock() {
+ function MyMock($test, $wildcard) {
+ $this->SimpleMock(&$test, $wildcard);
+ }
+
+ function _assertTrue($assertion, $message , &$test) {
+ $test->assertion($message, $assertion);
+ }
+ }
+ SimpleTestOptions::setMockBaseClass('MyMock');
+?>
+
+ From now on you just include my_mock.php instead of the
+ default mock_objects.php version and you can introduce
+ mock objects into your existing test suite.
+
+
+