Creating a new test case

If you are new to unit testing it is recommended that you actually try the code out as we go. There is actually not very much to type and you will get a feel for the rhythm of test first programming.

To run the examples as is, you need an empty directory with the folders classes, tests and temp. Unpack the SimpleTest framework into the tests folder and make sure your web server can reach these locations.

A new test case

The quick introductory example featured the unit testing of a simple log class. In this tutorial on SimpleTest I am going to try to tell the whole story of developing this class. This PHP class is small and simple and in the course of this introduction will receive far more attention than it probably would in production. Yet even this tiny class contains some surprisingly difficult design decisions.

Maybe they are too difficult? Rather than trying to design the whole thing up front I'll start with a known requirement, namely we want to write messages to a file. These messages must be appended to the file if it exists. Later we will want priorities and filters and things, but for now we will place the file writing requirement in the forefront of our thoughts. We will think of nothing else for fear of getting confused. OK, let's make a test...

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

class TestOfLogging extends UnitTestCase {
    function testCreatingNewFile() {
    }
}
?>
Piece by piece here is what it all means.

The SIMPLE_TEST constant is the path from this file to the Simple Test classes. The classes could be placed into the path in the php.ini file, but if you are using a shared hosting account you do not have access to this. To keep everybody happy this path is declared explicitely in the test script. Later we will see how it eventually ends up in only one place.

What is this autorun.php file? The Simple Test libraries are a toolkit for creating your own standardised test suite. They can be used "as is" without trouble, but consist of separate components that have to be assembled. autorun.php is a special component : it provides both the unit testing and display pieces. It catches all test classes and runs them automagically.

It is probable that you will eventually write your own display and so including the default set is optional. SimpleTest includes a usable, but basic, test display class called HtmlReporter. It can record test starts, ends, errors, passes and fails. It displays this information as quickly as possible in case the test code crashes the script and obscures the point of failure.

The tests themselves are gathered in test case classes. This one is typical in extending UnitTestCase. When the test is run it will search for any method within that starts with the name "test" and run it. Our only test method at present is called testCreatingNewFile(). There is nothing in it yet.

Now the empty method definition on its own does not do anything. We need to actually place some code inside it. The UnitTestCase class will typically generate test events when run and these events are sent to an observer.

Now to add test code...

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

class TestOfLogging extends UnitTestCase {
    function testCreatingNewFile() {
        @unlink('../temp/test.log');
        $log = new Log('../temp/test.log');
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('../temp/test.log'));
    }
}
?>

You are probably thinking that that is a lot of test code for just one test and I would agree. Don't worry. This is a fixed cost and from now on we can add tests pretty much as one liners. Even less when using some of the test artifacts that we will use later.

Now comes the first of our decisions. Our test file is called log_test.php (any name is fine) and is in a folder called tests (anywhere is fine). We have called our code file log.php and this is the code we are going to test. I have placed it into a folder called classes, so that means we are building a class, yes?

For this example I am, but the unit tester is not restricted to testing classes. It is just that object oriented code is easier to break down and redesign for testing. It is no accident that the fine grain testing style of unit tests has arisen from the object community.

The test itself is minimal. It first deletes any previous test file that may have been left lying around. Design decisions now come in thick and fast. Our class is called Log and takes the file path in the constructor. We create a log and immediately send a message to it using a method named message(). Sadly, original naming is not a desirable characteristic of a software developer.

The smallest unit of a...er...unit test is the assertion. Here we want to assert that the log file we just sent a message to was indeed created. UnitTestCase::assertTrue() will send a pass event if the condition evaluates to true and a fail event otherwise. We can have a variety of different assertions and even more if we extend our base test cases. Here is the base list...

assertTrue($x)Fail if $x is false
assertFalse($x)Fail if $x is true
assertNull($x)Fail if $x is set
assertNotNull($x)Fail if $x not set
assertIsA($x, $t)Fail if $x is not the class or type $t
assertEqual($x, $y)Fail if $x == $y is false
assertNotEqual($x, $y)Fail if $x == $y is true
assertIdentical($x, $y)Fail if $x === $y is false
assertNotIdentical($x, $y)Fail if $x === $y is true
assertReference($x, $y)Fail unless $x and $y are the same variable
assertCopy($x, $y)Fail if $x and $y are the same variable
assertWantedPattern($p, $x)Fail unless the regex $p matches $x
assertNoUnwantedPattern($p, $x)Fail if the regex $p matches $x
assertNoErrors()Fail if any PHP error occoured
assertError($x)Fail if no PHP error or incorrect message

We are now ready to execute our test script by pointing the browser at it. What happens? It should crash...

Fatal error: Failed opening required '../classes/log.php' (include_path='') in /home/marcus/projects/lastcraft/tutorial_tests/Log/tests/log_test.php on line 7
The reason is that we have not yet created log.php.

Hang on, that's silly! You aren't going to build a test without creating any of the code you are testing, surely...?

Test Driven Development

Co-inventor of Extreme Programming, Kent Beck, has come up with another manifesto. The book is called Test driven development or TDD and raises unit testing to a senior position in design. In a nutshell you write a small test first and then only get this passing by writing code. Any code. Just get it working.

You write another test and get that passing. What you will now have is some duplication and generally lousy code. You re-arrange (refactor) that code while the tests are passing and thus while you cannot break anything. Once the code is as clean as possible you are ready to add more functionality. In turn you only achieve this by adding another test and starting the cycle again.

This is a radical approach and one that I feel is incomplete. However it makes for a great way to explain a unit tester! We happen to have a failing, not to say crashing, test right now so let's write some code into log.php...

<?php
class Log {
    
    function Log($file_path) {
    }
        
    function message($message) {
    }
}
?>
This is the minimum to avoid a PHP fatal error. We now get the response...

testoflogging

Fail: testcreatingnewfile->True assertion failed.
1/1 test cases complete. 0 passes and 1 fails.
And "testoflogging" has failed. PHP has this really annoying effect of reducing class and method names to lower case internally. SimpleTest uses these names by default to describe the tests, but we can replace them with our own...
class TestOfLogging extends UnitTestCase {
    function TestOfLogging() {
        $this->UnitTestCase('Log class test');
    }
    function testCreatingNewFile() {
        @unlink('../temp/test.log');
        $log = new Log('../temp/test.log');
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('../temp/test.log'), 'File created');
    }
}
Giving...

Log class test

Fail: testcreatingnewfile->File created.
1/1 test cases complete. 0 passes and 1 fails.
There is not much we can do about the method name I am afraid.

Test messages like this are a bit like code comments. Some organisations insist on them while others ban them as clutter and a waste of good typing. I am somewhere in the middle.

To get the test passing we could just create the file in the Log constructor. This "faking it" technique is very useful for checking that your tests work when the going gets tough. This is especially so if you have had a run of test failures and just want to confirm that you haven't just missed something stupid. We are not going that slow, so...

<?php   
class Log {
    var $_file_path;
        
    function Log($file_path) {
        $this->_file_path = $file_path;
    }
        
    function message($message) {
        $file = fopen($this->_file_path, 'a');
        fwrite($file, $message . "\n");
        fclose($file);
    }
}
?>
It took me no less than four failures to get to the next step. I had not created the temporary directory, I had not made it publicly writeable, I had one typo and I did not check in the new directory to CVS. Any one of these could have kept me busy for several hours if they had come to light later, but then that is what testing is for. With the necessary fixes we get...

Log class test

1/1 test cases complete. 1 passes and 0 fails.
Success!

You may not like the rather minimal style of the display. Passes are not shown by default because generally you do not need more information when you actually understand what is going on. If you do not know what is going on then you should write another test.

OK, this is a little strict. If you want to see the passes as well then you can subclass the HtmlReporter class and attach that to the test instead. Even I like the comfort factor sometimes.

Tests as Documentation

There is a subtlety here. We don't want the file created until we actually send a message. Rather than think about this too deeply we will just add another test for it...

class TestOfLogging extends UnitTestCase {
    function TestOfLogging() {
        $this->UnitTestCase('Log class test');
    }
    function testCreatingNewFile() {
        @unlink('../temp/test.log');
        $log = new Log('../temp/test.log');
        $this->assertFalse(file_exists('../temp/test.log'), 'No file created before first message');
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('../temp/test.log'), 'File created');
    }
}
...and find it already works...

Log class test

1/1 test cases complete. 2 passes and 0 fails.
Actually I knew it would. I am putting this test in to confirm this partly for peace of mind, but also to document the behaviour. That little extra test line says more in this context than a dozen lines of use case or a whole UML activity diagram. That the test suite acts as a source of documentation is a pleasant side effect of all these tests.

Should we clean up the temporary file at the end of the test? I usually do this once I am finished with a test method and it is working. I don't want to check in code that leaves remnants of test files lying around after a test. I don't do it while I am writing the code, though. I probably should, but sometimes I need to see what is going on and there is that comfort thing again.

In a real life project we usually have more than one test case, so we next have to look at grouping tests into test suites.

Creating a new test case.
Test driven development in PHP.
Tests as documentation is one of many side effects.
The JUnit FAQ has plenty of useful testing advice.
Next is grouping test cases together.
You will need the SimpleTest testing framework for these examples.