Mock Objects

Refactoring the tests again

Before more functionality is added there is some refactoring to do. We are going to do some timing tests and so the TimeTestCase class definitely needs its own file. Let's say tests/time_test_case.php...

<?php
if (! defined('SIMPLE_TEST')) {
    define('SIMPLE_TEST', 'simpletest/');
}
require_once(SIMPLE_TEST . 'unit_tester.php');

class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name = '') {
        $this->UnitTestCase($test_name);
    }
    function assertSameTime($time1, $time2, $message = '') {
        if (! $message) {
            $message = "Time [$time1] should match time [$time2]";
        }
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
}
?>
We can then require() this file into the all_tests.php script.

Adding a timestamp to the Log

I don't know quite what the format of the log message should be for the test, so to check for a timestamp we could do the simplest possible thing, which is to look for a sequence of digits.

<?php
require_once('../classes/log.php');
require_once('../classes/clock.php');

class TestOfLogging extends TimeTestCase {
    function TestOfLogging() {
        $this->TimeTestCase('Log class test');
    }
    function setUp() {
        @unlink('../temp/test.log');
    }
    function tearDown() {
        @unlink('../temp/test.log');
    }
    function getFileLine($filename, $index) {
        $messages = file($filename);
        return $messages[$index];
    }
    function testCreatingNewFile() {
        ...
    }
    function testAppendingToFile() {
        ...
    }
    function testTimestamps() {
        $log = new Log('../temp/test.log');
        $log->message('Test line');
        $this->assertTrue(
                preg_match('/(\d+)/', $this->getFileLine('../temp/test.log', 0), $matches),
                'Found timestamp');
        $clock = new clock();
        $this->assertSameTime((integer)$matches[1], $clock->now(), 'Correct time');
    }
}
?>
The test case creates a new Log object and writes a message. We look for a digit sequence and then test it against the current time using our Clock object. Of course it doesn't work until we write the code.

All tests

Pass: log_test.php->Log class test->testappendingtofile->Expecting [/Test line 1/] in [Test line 1]
Pass: log_test.php->Log class test->testappendingtofile->Expecting [/Test line 2/] in [Test line 2]
Pass: log_test.php->Log class test->testcreatingnewfile->Created before message
Pass: log_test.php->Log class test->testcreatingnewfile->File created
Fail: log_test.php->Log class test->testtimestamps->Found timestamp

Notice: Undefined offset: 1 in /home/marcus/projects/lastcraft/tutorial_tests/tests/log_test.php on line 44
Fail: log_test.php->Log class test->testtimestamps->Correct time
Pass: clock_test.php->Clock class test->testclockadvance->Advancement
Pass: clock_test.php->Clock class test->testclocktellstime->Now is the right time
3/3 test cases complete. 6 passes and 2 fails.
The test suite is still showing the passes from our earlier modification.

We can get the tests to pass simply by adding a timestamp when writing out to the file. Yes, of course all of this is trivial and I would not normally test this fanatically, but it is going to illustrate a more general problem. The log.php file becomes...

<?php
require_once('../classes/clock.php');
    
class Log {
    var $_file_path;
        
    function Log($file_path) {
        $this->_file_path = $file_path;
    }
        
    function message($message) {
        $clock = new Clock();
        $file = fopen($this->_file_path, 'a');
        fwrite($file, "[" . $clock->now() . "] $message\n");
        fclose($file);
    }
}
?>
The tests should now pass.

Our new test is full of problems, though. What if our time format changes to something else? Things are going to be a lot more complicated to test if this happens. It also means that any changes to the clock class time format will cause our logging tests to fail also. This means that our log tests are tangled up with the clock tests and extremely fragile. It lacks cohesion, which is the same as saying it is not tightly focused, testing facets of the clock as well as the log. Our problems are caused in part because the clock output is unpredictable when all we really want to test is that the logging message contains the output of Clock::now(). We don't really care about the contents of that method call.

Can we make that call predictable? We could if we could get the log to use a dummy version of the clock for the duration of the test. The dummy clock class would have to behave the same way as the Clock class except for the fixed output from the now() method. Hey, that would even free us from using the TimeTestCase class!

We could write such a class pretty easily although it is rather tedious work. We just create another clock class with same interface except that the now() method returns a value that we can change with some other setter method. That is quite a lot of work for a pretty minor test.

Except that it is really no work at all.

A mock clock

To reach instant testing clock nirvana we need only three extra lines of code...

require_once('simpletest/mock_objects.php');
This includes the mock generator code. It is simplest to place this in the all_tests.php script as it gets used rather a lot.
Mock::generate('Clock');
This is the line that does the work. The code generator scans the class for all of its methods, creates code to generate an identically interfaced class, but with the name "Mock" added, and then eval()s the new code to create the new class.
$clock = &new MockClock($this);
This line can be added to any test method we are interested in. It creates the dummy clock ready to receive our instructions.

Our test case is on the first steps of a radical clean up...

<?php
require_once('../classes/log.php');
require_once('../classes/clock.php');
Mock::generate('Clock');

class TestOfLogging extends UnitTestCase {
    function TestOfLogging() {
        $this->UnitTestCase('Log class test');
    }
    function setUp() {
        @unlink('../temp/test.log');
    }
    function tearDown() {
        @unlink('../temp/test.log');
    }
    function getFileLine($filename, $index) {
        $messages = file($filename);
        return $messages[$index];
    }
    function testCreatingNewFile() {
        ...
    }
    function testAppendingToFile() {
        ...
    }
    function testTimestamps() {
        $clock = &new MockClock($this);
        $clock->setReturnValue('now', 'Timestamp');
        $log = new Log('../temp/test.log');
        $log->message('Test line', &$clock);
        $this->assertWantedPattern(
                '/Timestamp/',
                $this->getFileLine('../temp/test.log', 0),
                'Found timestamp');
    }
}
?>
This test method creates a MockClock object and then sets the return value of the now() method to be the string "Timestamp". Every time we call $clock->now() it will return this string. This should be easy to spot.

Next we create our log and send a message. We pass into the message() call the clock we would like to use. This means that we will have to add an optional parameter to the logging class to make testing possible...

class Log {
    var $_file_path;
    
    function Log($file_path) {
        $this->_file_path = $file_path;
    }
    
    function message($message, $clock = false) {
        if (!is_object($clock)) {
            $clock = new Clock();
        }
        $file = fopen($this->_file_path, 'a');
        fwrite($file, "[" . $clock->now() . "] $message\n");
        fclose($file);
    }
}
All of the tests now pass and they test only the logging code. We can breathe easy again.

Does that extra parameter in the Log class bother you? We have changed the interface just to facilitate testing after all. Are not interfaces the most important thing? Have we sullied our class with test code?

Possibly, but consider this. Next chance you get, look at a circuit board, perhaps the motherboard of the computer you are looking at right now. On most boards you will find the odd empty hole, or solder joint with nothing attached or perhaps a pin or socket that has no obvious function. Chances are that some of these are for expansion and variations, but most of the remainder will be for testing.

Think about that. The factories making the boards many times over wasting material on parts that do not add to the final function. If hardware engineers can make this sacrifice of elegance I am sure we can too. Our sacrifice wastes no materials after all.

Still bother you? Actually it bothers me too, but not so much here. The number one priority is code that works, not prizes for minimalism. If it really bothers you, then move the creation of the clock into another protected factory method. Then subclass the clock for testing and override the factory method with one that returns the mock. Your tests are clumsier, but your interface is intact.

Again I leave the decision to you.

Refactoring the tests so we can reuse our new time test.
Adding Log timestamps.
Mocking the clock to make the test cohesive.
This follows the unit test tutorial.
Next is distilling boundary classes.
You will need the SimpleTest tool to run the examples.
Mock objects papers.