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