Subclassing a unit test case

A timing insensitive assertion

We left our clock test with a hole. If the PHP time() function rolled over during this comparison...

function testClockTellsTime() {
    $clock = new Clock();
    $this->assertEqual($clock->now(), time(), 'Now is the right time');
}

...our test would be out by one second and would cause a false failure. Erratic behaviour of our test suite is not what we want when we could be running it a hundred times a day.

We could rewrite the test as...

function testClockTellsTime() {
    $clock = new Clock();
    $time1 = $clock->now();
    $time2 = time();
    $this->assertTrue($time1 == $time2) || ($time1 + 1 == $time2), 'Now is the right time');
}

This is hardly a clear design though and we will have to repeat this for every timing test that we do. Repetition is public enemy number one and so we'll use this as incentive to factor out the new test code.

class TestOfClock extends UnitTestCase {
    function TestOfClock() {
        $this->UnitTestCase('Clock class test');
    }
    function assertSameTime($time1, $time2, $message) {
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertSameTime($clock->now(), time(), 'Now is the right time');
    }
    function testClockAdvance() {
        $clock = new Clock();
        $clock->advance(10);
        $this->assertSameTime($clock->now(), time() + 10, 'Advancement');
    }
}

Of course each time I make one of these changes I rerun the tests to make sure we are still OK. Refactor on green. It's a lot safer.

Reusing our assertion

It may be that we want more than one test case that is timing sensitive. Perhaps we are reading timestamps from database rows or other places that could allow an extra second to tick over. For these new test classes to take advantage of our new assertion we need to place it into a superclass.

Here is the complete clock_test.php file after promoting our assertSameTime() method to its own superclass...

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

class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name) {
        $this->UnitTestCase($test_name);
    }
    function assertSameTime($time1, $time2, $message) {
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
}
    
class TestOfClock extends TimeTestCase {
    function TestOfClock() {
        $this->TimeTestCase('Clock class test');
    }
    function testClockTellsTime() {
        $clock = new Clock();
        $this->assertSameTime($clock->now(), time(), 'Now is the right time');
    }
    function testClockAdvance() {
        $clock = new Clock();
        $clock->advance(10);
        $this->assertSameTime($clock->now(), time() + 10, 'Advancement');
    }
}
?>

Now we get the benefit of our new assertion every time we inherit from our own TimeTestCase class rather than the default UnitTestCase. This is very much how the JUnit tool was designed to be used and SimpleTest is a port of that interface. It is a testing framework from which your own test system can be grown.

If we run the tests now we get a slight niggle...

Warning: Missing argument 1 for timetestcase() in /home/marcus/projects/lastcraft/tutorial_tests/tests/clock_test.php on line 5

All tests

3/3 test cases complete. 6 passes and 0 fails.
The reasons for this are quite tricky.

Our subclass requires a constructor parameter that has not been supplied and yet it appears that we did supply it. When we inherited our new class we passed it in our own constructor. It's right here...

function TestOfClock() {
    $this->TimeTestCase('Clock class test');
}

In fact we are right, that is not the problem.

Remember when we built our all_tests.php group test by using the addFile() method. This method looks for test case classes, instantiates them if they are new and then runs all of their tests. What's happened is that it has found our test case extension as well. This is harmless as there are no test methods within it, that is, method names that start with the string "test". No extra tests are run.

The trouble is that it instantiates the class and does this without the $test_name parameter which is what causes our warning. This parameter is not normally required of a test case and not normally of its assertions either. To make our extended test case match the UnitTestCase interface we must make these optional...

class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name = false) {
        $this->UnitTestCase($test_name);
    }
    function assertSameTime($time1, $time2, $message = false) {
        if (! $message) {
            $message = "Time [$time1] should match time [$time2]";
        }
        $this->assertTrue(
                ($time1 == $time2) || ($time1 + 1 == $time2),
                $message);
    }
}

Of course it should still bother you that this class is instantiated by the test suite unnecessarily. Here is a modification to prevent it running...

SimpleTestOptions::ignore('TimeTestCase');
class TimeTestCase extends UnitTestCase {
    function TimeTestCase($test_name = false) {
        $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);
    }
}

This just tells SimpleTest to always ignore this class when building test suites. It can be included anywhere in the test case file.

Six passes looks good, but does not tell the casual observer what has been tested. For that you have to look at the code. If that sounds like drudge to you and you would like this information displayed before you then we should go on to show the passes next.

A timing insensitive assertion that allows a one second gain.
Subclassing the test case so as to reuse the test method.
The previous section was controlling test variables.
The next tutorial section was changing the test display.
You will need the SimpleTest test tool to run the sample code.