UnitTesting in PHP using SimpleTest


crcr(If you haven’t downloaded SimpleTest framework yet, click here.)

You will probably become a Test-infected developer if you try a unit testing framework! But what is unit testing?

The basic concept of unit testing is write more code which will test the main code we’ve written, by “throwing” sample data at it and examining what it gets back.“-Harry Fuecks.

Some developers might not like the idea of writing more code but it will save you a good time pin pointing the exact piece of code causing the problem when your script becomes large.

Time to get practical about UnitTesting! We will write a test case for a simple file manipulation class. This file class will have the basic operations needed to manipulate files. It will be able to create, read, write, append and delete files and our test case should test each operation individually.
First, here is the definition of the class:
[file.class.php]

<?php
class File
{
var $filename;

/**
* Constructor
* will take care of creating the file if it does not exists.
* @param string $file
*/
function File($file)
{
if (empty($file))
die(‘Filename should not be empty’);

$this->filename = $file;

//if the file does not exists and we can not create it, terminate the script
if (!$this->exists() && !$this->create())
die(‘Unable to find/create the file’);
}

/**
* checks if the file exists or not!
* @return bool
*/
function exists()
{
return file_exists($this->filename);
}

/**
* This method will attempt to create a file if it does not exists.
* @return bool
*/
function create()
{
//if the file exists, save the trouble opening it.
if ($this->exists())
return true;

return $this->putContents(null);
}

/**
* reads the file’s contents and returns it..
* @return string
*/
function getContents()
{
return @file_get_contents($this->filename);
}

/**
* writes to the file..
* @param string $content
* @return bool
*/
function putContents($content)
{
$fp = @fopen($this->filename, ‘wb’);
$write = @fwrite($fp, $content);
@fclose($fp);

return !($write === false);
}

/**
* append data to the file..
* @param string $content
* @return bool
*/
function append($content)
{
$fp = @fopen($this->filename, ‘ab’);
$append = @fwrite($fp, $content);
@fclose($fp);

return !($append === false);
}

/**
* deletes the file..
* @return bool
*/
function delete()
{
return @unlink($this->filename);
}
}
?>

I will assume that you are able to understand how the code works since we are talking about unit testing which is far more advanced than file manipulation.
If you can’t understand how this class works, I suggest you learn first about OOP then learn about unit testing.
Anyway, it’s not the best file class out there because I created it just for this tutorial so don’t get mad about it ;)

Writing The Test Case

Now we want to create a test case for this class to see if it works as we expect or not!
To create a test case we have to create a new class which inherits from UnitTestCase class which is found in the file unit_tester.php which comes in SimpleTest framework package.
So we create our new PHP file, let’s name it fileTest.php and include unit_tester.php and our file file.class.php:
[fileTest.php]

<?php
include_once 'simpletest/unit_tester.php';

include_once
'file.class.php';

Of course you have to change the paths to the correct paths in your environment.
We now start the definition of the test case class and let’s name it fileTest as well:

class fileTest extends UnitTestCase
{
var $file;
var $filename;

function fileTest($filename)
{
$this->filename = $filename;
$this->file = new file($this->filename);
$this->UnitTestCase(‘File Manipulation Test’);
}

function setUp()
{
$fp = @fopen($this->filename, ‘x’);
if (!($fp === false)) {
fwrite($fp, ”);
fclose($fp);
}
}

function tearDown()
{
@unlink($this->filename);
}

As you can see, we have defined 3 methods (including the constructor) and 2 data members, $file and $filename.
The constructor (fileTest) takes one argument which is the file name and assign it to the data member $filename and create the file object and assign it to $file.
The line:  $this->UnitTestCase(‘File Manipulation Test’);  is optional and it just makes a header line in the test reporter (we will talk about this later).

Also we have defined the methods setUp() and tearDown().
setUp() will be called automatically before each and every test and tearDown() will be called after each and every test. However, these two methods are optional.

We have defined the setUp() method to create a new clean file before each test and tearDown() will delete that file after each test.

Now let’s write our first test! To create a test you simply have to define a method which starts with the word “test”.
It doesn’t matter if you use an underscore after the word “test” or not! As long as “test” is the first word, it doesn’t matter. Let’s see our first test:

function testFileExists()
{
$this->assertTrue($this->file->exists());
clearstatcache();
$this->assertTrue(file_exists($this->file->filename));
}

In this test, we check for the file existence twice (paranoid?) using the method assertTrue() which will insure that the method exists() will return true!
Then  we use the built-in function file_exists() to double check that the file truely exists!
I’ll tell you later why we checked twice.

UnitTesting in SimpleTest depends heavily on assertions! Like in our previous test we used the method assertTrue() which will require the argument passed to it to be true in order to pass or it will fail!
It’s worth saying that assertTrue() will pass as long as the argument is neither 0 nor false. So assertTrue(“String”) will pass.
If you want to be strict about this, use assertIdentical() which will check the value and the type. e.g. assertIdentical(“true”, true) will fail!

SimpleTest has many assert*() methods and in our UnitTestCase class we have these:

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
assertNotA($x, $t) Fail if $x is of 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 or a type mismatch
assertNotIdentical($x, $y) Fail if $x == $y is true and types match
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
assertErrorPattern($p) Fail unless the error matches the regex $p

We will only need some these assestions to test our current unit :)

Note that you could pass additional argument to all these methods which will be the message to be displayed in case the test fails e.g. $this->assertTrue(false, ‘Passing false to assertTrue faild!’).
Don’t worry though, because the error messages generated by SimpleTest are very informative so no need to bother writing new messages.

Now, let’s proceed to the next test:

function testCreate()
{
$this->tearDown();
$this->assertFalse(file_exists($this->file->filename));
clearstatcache();

$this->assertTrue($this->file->create());
$this->assertTrue(file_exists($this->file->filename));
}

This test will test the create() method and makes sure it’s creating the file!

We first delete the file created (automatically) by setUp() by using tearDown() and then check and insure that the file no longer exists by using assertFalse() which will pass only if file_exists() returned false (file does not exists).
We then check to see if the method create() successfully created the file and returned true in the last two lines of the test.

Needless to say that it’s more reading friendly to make the test name similar or related to the method/part you are testing.

We’ll now see a new assertion type in the next test:

function testReadWrite()
{
$contents = 'contents';
$this->assertTrue($this->file->putContents($contents));
$this->assertEqual($this->file->getContents(), $contents);
}

This test will check the putContents() and getContents() methods.
We will write some contents to the file using putContents() and make sure everything went ok and then use assertEqual() to make sure that what we’d written in the file is equal to what we get from getContents().

So you think I am paranoid when I check one part twice? It will be useless to just check for putContents() since it might pass the test but without actually doing what expected by just returning true or maybe because we had defined the method incorrectly! So we have to check the contents of this file and insure they are equal.
This is why we wrote two tests to check file existence and creation as well.

Now, how many operations left to test? Two, right?
Here is the test for the method append():

function testAppend()
{
$this->file->putContents("SimpleTest");
$this->assertTrue($this->file->append("NeverMind"));
$this->assertWantedPattern('~nevermind$~i', $this->file->getContents());
}

Note that we didn’t test the method putContents() again since it was tested before (tests are performed in the order they had been written in).
We use a new assertion method that is assertWantedPattern() which will try to match the regular expression pattern in the first argument against the second argument.
We want to make sure that the word “NeverMind” is found at the end of the file so the $ sign in regexp will help us.

And finally, our last test:

function testDelete()
{
$this->assertTrue($this->file->delete());
$this->assertFalse(file_exists($this->filename));
}

There is no new assertion type used here and what I want to test is clear, so I need not say anything here, do I? It’s pretty obvious :)

Running The Test Case

So now all our tests are ready. We then instantiate the test case by:

$test = new fileTest('test.txt');

Now try running this script….
Nothing appeared, right? Remember when we talked about the line $this->UnitTestCase(‘File Manipulation Test’) and how it’s optional and it will only create a header in the reporter? Yes, We have to use the reporter to show how the tests went!

To use the reporter, we have to include the file reporter.php (at the top for convenience) so our file top should look like this:

include_once 'simpletest/unit_tester.php';
include_once 'simpletest/reporter.php';

include_once ‘file.class.php’;

and then create the reporter and run it by the test case object by adding this line at the end:

$test = new fileTest('test.txt');
$test->run(new HtmlReporter());

Finally, our test case script should be like this:
[fileTest.php]

<?php
include_once 'simpletest/unit_tester.php';
include_once 'simpletest/reporter.php';

include_once ‘file.class.php’;

class fileTest extends UnitTestCase
{
var $file;
var $filename;

function fileTest($filename)
{
$this->filename = $filename;
$this->file = new file($this->filename);
$this->UnitTestCase(‘File Manipulation Test’);
}

function setUp()
{
$fp = @fopen($this->filename, ‘x’);
if (!($fp === false)) {
fwrite($fp, ”);
fclose($fp);
}
}

function tearDown()
{
@unlink($this->filename);
}

function testFileExists()
{
$this->assertTrue($this->file->exists());
clearstatcache();
$this->assertTrue(file_exists($this->file->filename));
}

function testCreate()
{
$this->tearDown();
$this->assertFalse(file_exists($this->file->filename));
clearstatcache();

$this->assertTrue($this->file->create());
$this->assertTrue(file_exists($this->file->filename));
}

function testReadWrite()
{
$contents = ‘contents’;
$this->assertTrue($this->file->putContents($contents));
$this->assertEqual($this->file->getContents(), $contents);
}

function testAppend()
{
$this->file->putContents(“SimpleTest”);
$this->assertTrue($this->file->append(“NeverMind”));
$this->assertWantedPattern(‘~nevermind$~i’, $this->file->getContents());
}

function testDelete()
{
$this->assertTrue($this->file->delete());
$this->assertFalse(file_exists($this->filename));
}
}
$test = new fileTest(‘test.txt’);
$test->run(new HtmlReporter());
?>

Now run the script again and if everything goes fine you should see something similar to this if all tests passed:

File Manipulation Test

1/1 test cases complete: 11 passes, 0 fails and 0 exceptions.

In case we had any failure, you will see something similar to:

File Manipulation Test

Fail: testAppend -> Pattern [~^nevermind$~i] not detected in [String: SimpleTestNeverMind] at line [61]

1/1 test cases complete: 10 passes, 1 fails and 0 exceptions.

As you can see, there was a failure in the method testAppend() and the error message has clearly stated what and where the error is.
Note that this test failed because I edited the pattern and forced the contents of the file to be “NeverMind” only in order to pass (case-insensitive) which, of course, made the test fails.

In case you had any failures, the script will continue and will not terminate until all tests are performed.
If any PHP errors occurred, such as warnings or notices, they will be caught and reported as exceptions.

Conclusion

Now after all this effort, you might ask: “Why bother writing all these tests?” I tell you it’s much better than debugging with nasty ways such as var_dump()’s and difficult investigations to find if there were any typos or null statments or whatever might be causing the problem. With unit testing, the test case will report were exactly is the problem.
With unit testing, if you ever change your class implementation, all you have to do is run the test case and it will report if everything is functioning correctly or not and give details about any failures, if any.
Another advantage is that you are separating the debugging code from the original code! No need to clean up after debugging or anything.

A final remark, SimpleTest has great potential and many other features like mock objects and web tester which could make your debugging process easier than ever! for example, the web tester can navigate your website, follow links, click buttons, submit forms and much more!
In this article, we were demonstrating the most basic use of unit testing. It is just a kick off to infect you with the Test-infection :D
If you want to learn more about this great framework, read the docs at LastCraft.

Author: Saleh Jamal
Related Posts:

Leave a Comment