Simple Test for PHP

This page...

The following assumes that you are familiar with the concept of unit testing as well as the PHP web development language. It is a guide for the impatient new user of SimpleTest. For fuller documentation, especially if you are new to unit testing see the ongoing documentation, and for example test cases see the unit testing tutorial.

Using the tester quickly

Amongst software testing tools, a unit tester is the one closest to the developer. In the context of agile development the test code sits right next to the source code as both are written simultaneously. In this context SimpleTest aims to be a complete PHP developer test solution and is called "Simple" because it should be easy to use and extend. It wasn't a good choice of name really. It includes all of the typical functions you would expect from JUnit and the PHPUnit ports, and includes mock objects.

What makes this tool immediately useful to the PHP developer is the internal web browser. This allows tests that navigate web sites, fill in forms and test pages. Being able to write these test in PHP means that it is easy to write integrated tests. An example might be confirming that a user was written to a database after a signing up through the web site.

The quickest way to demonstrate SimpleTest is with an example.

Let us suppose we are testing a simple file logging class called Log in classes/log.php. We start by creating a test script which we will call tests/log_test.php and populate it as follows...

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

class TestOfLogging extends UnitTestCase {
}
?>
Here the simpletest folder is either local or in the path. You would have to edit these locations depending on where you unpacked the toolset. The "autorun.php" file does more than just include the SimpleTest files, it also runs our test for us.

The TestOfLogging is our first test case and it's currently empty. Each test case is a class that extends one of the SimpleTet base classes and we can have as many of these in the file as we want.

With three lines of scaffolding, and our Log class include, we have a test suite. No tests though.

For our first test, we'll assume that the Log class takes the file name to write to in the constructor, and we have a temporary folder in which to place this file...

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

class TestOfLogging extends UnitTestCase {
    function testLogCreatesNewFileOnFirstMessage() {
        @unlink('/temp/test.log');
        $log = new Log('/temp/test.log');
        $this->assertFalse(file_exists('/temp/test.log'));
        $log->message('Should write this to a file');
        $this->assertTrue(file_exists('/temp/test.log'));
    }
}
?>
When a test case runs, it will search for any method that starts with the string "test" and execute that method. If the method starts "test", it's a test. Note the very long name testLogCreatesNewFileOnFirstMessage(). This is considered good style and makes the test output more readable.

We would normally have more than one test method in a test case, but that's for later.

Assertions within the test methods trigger messages to the test framework which displays the result immediately. This immediate response is important, not just in the event of the code causing a crash, but also so that print statements can display their debugging content right next to the assertion concerned.

To see these results we have to actually run the tests. No other code is necessary - we can just open the page with our browser.

On failure the display looks like this...

TestOfLogging

Fail: testLogCreatesNewFileOnFirstMessage->True assertion failed.
1/1 test cases complete. 1 passes and 1 fails.
...and if it passes like this...

TestOfLogging

1/1 test cases complete. 2 passes and 0 fails.
And if you get this...
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
it means you're missing the classes/Log.php file that could look like...
<?php
class Log {
    function Log($file_path) {
    }

    function message() {
    }
}
?>
It's fun to write the code after the test. More than fun even - this system is called "Test Driven Development".

For more information about UnitTestCase, see the unit test documentation.

Building test suites

It is unlikely in a real application that we will only ever run one test case. This means that we need a way of grouping cases into a test script that can, if need be, run every test for the application.

Our first step is to create a new file called tests/all_tests.php and insert the following code...

<?php
require_once('simpletest/autorun.php');

class AllTests extends TestSuite {
    function AllTests() {
        $this->TestSuite('All tests');
        $this->addFile('log_test.php');
    }
}
?>
The "autorun" include allows our upcoming test suite to be run just by invoking this script.

The TestSuite subclass must chain it's constructor. This limitation will be removed in future versions.

The method TestSuite::addFile() will include the test case file and read any new classes that are descended from SimpleTestCase. UnitTestCase is just one example of a class derived from SimpleTestCase, and you can create your own. TestSuite::addFile() can include other test suites.

The class will not be instantiated yet. When the test suite runs it will construct each instance once it reaches that test, then destroy it straight after. This means that the constructor is run just before each run of that test case, and the destructor is run before the next test case starts.

It is common to group test case code into superclasses which are not supposed to run, but become the base classes of other tests. For "autorun" to work properly the test case file should not blindly run any other test case extensions that do not actually run tests. This could result in extra test cases being counted during the test run. Hardly a major problem, but to avoid this inconvenience simply mark your base class as abstract. SimpleTest won't run abstract classes. If you are still using PHP4, then a SimpleTestOptions::ignore() directive somewhere in the test case file will have the same effect.

Also, the test case file should not have been included elsewhere or no cases will be added to this group test. This would be a more serious error as if the test case classes are already loaded by PHP the TestSuite::addFile() method will not detect them.

To display the results it is necessary only to invoke tests/all_tests.php from the web server or the command line.

For more information about building test suites, see the test suite documentation.

Using mock objects

Let's move further into the future and do something really complicated.

Assume that our logging class is tested and completed. Assume also that we are testing another class that is required to write log messages, say a SessionPool. We want to test a method that will probably end up looking like this...


class SessionPool {
    ...
    function logIn($username) {
        ...
        $this->_log->message("User $username logged in.");
        ...
    }
    ...
}

In the spirit of reuse, we are using our Log class. A conventional test case might look like this...
<?php
require_once('simpletest/autorun.php');
require_once('../classes/log.php');
require_once('../classes/session_pool.php');

class TestOfSessionLogging extends UnitTestCase {
    
    function setUp() {
        @unlink('/temp/test.log');
    }
    
    function tearDown() {
        @unlink('/temp/test.log');
    }
    
    function testLoggingInIsLogged() {
        $log = new Log('/temp/test.log');
        $session_pool = &new SessionPool($log);
        $session_pool->logIn('fred');
        $messages = file('/temp/test.log');
        $this->assertEqual($messages[0], "User fred logged in.\n");
    }
}
?>
We'll explain the setUp() and tearDown() methods later.

This test case design is not all bad, but it could be improved. We are spending time fiddling with log files which are not part of our test. We have created close ties with the Log class and this test. What if we don't use files any more, but use ths syslog library instead? It means that our TestOfSessionLogging test will fail, even thouh it's not testing Logging.

It's fragile in smaller ways too. Did you notice the extra carriage return in the message? Was that added by the logger? What if it also added a time stamp or other data?

The only part that we really want to test is that a particular message was sent to the logger. We can reduce coupling if we pass in a fake logging class that simply records the message calls for testing, but takes no action. It would have to look exactly like our original though.

If the fake object doesn't write to a file then we save on deleting the file before and after each test. We could save even more test code if the fake object would kindly run the assertion for us.

Too good to be true? We can create such an object easily...
<?php
require_once('simpletest/autorun.php');
require_once('../classes/log.php');
require_once('../classes/session_pool.php');

Mock::generate('Log');

class TestOfSessionLogging extends UnitTestCase {
    
    function testLoggingInIsLogged() {
        $log = &new MockLog();
        $log->expectOnce('message', array('User fred logged in.'));
        $session_pool = &new SessionPool($log);
        $session_pool->logIn('fred');
    }
}
?>
The Mock::generate() call code generated a new class called MockLog. This looks like an identical clone, except that we can wire test code to it. That's what expectOnce() does. It says that if message() is ever called on me, it had better be with the parameter "User fred logged in.".

The test will be triggered when the call to message() is invoked on the MockLog object by SessionPool::logIn() code. The mock call will trigger a parameter comparison and then send the resulting pass or fail event to the test display. Wildcards can be included here too, so you don't have to test every parameter of a call when you only want to test one.

If the mock reaches the end of the test case without the method being called, the expectOnce() expectation will trigger a test failure. In other words the mocks can detect the absence of behaviour as well as the presence.

The mock objects in the SimpleTest suite can have arbitrary return values set, sequences of returns, return values selected according to the incoming arguments, sequences of parameter expectations and limits on the number of times a method is to be invoked.

For more information about mocking and stubbing, see the mock objects documentation.

Web page testing

One of the requirements of web sites is that they produce web pages. If you are building a project top-down and you want to fully integrate testing along the way then you will want a way of automatically navigating a site and examining output for correctness. This is the job of a web tester.

The web testing in SimpleTest is fairly primitive, as there is no JavaScript. Most other browser operations are simulated.

To give an idea here is a trivial example where a home page is fetched, from which we navigate to an "about" page and then test some client determined content.

<?php
require_once('simpletest/autorun.php');
require_once('simpletest/web_tester.php');

class TestOfAbout extends WebTestCase {
    function testOurAboutPageGivesFreeReignToOurEgo() {
        $this->get('http://test-server/index.php');
        $this->click('About');
        $this->assertTitle('About why we are so great');
        $this->assertText('We are really great');
    }
}
?>
With this code as an acceptance test, you can ensure that the content always meets the specifications of both the developers, and the other project stakeholders.

You can navigate forms too...

<?php
require_once('simpletest/autorun.php');
require_once('simpletest/web_tester.php');

class TestOfRankings extends WebTestCase {
    function testWeAreTopOfGoogle() {
        $this->get('http://google.com/');
        $this->setField('q', 'simpletest');
        $this->click("I'm Feeling Lucky");
        $this->assertTitle('SimpleTest - Unit Testing for PHP');
    }
}
?>
...although this could violate Google's(tm) terms and conditions.

For more information about web testing, see the scriptable browser documentation and the WebTestCase.

SourceForge.net Logo

References and related information...