Now that you know what unit testing is and how to write and run tests, it’s time to look at writing tests in more depth.
Today we’ll take an example class and write tests for it. We’ll also introduce some common testing methodologies.
Our example class
To do some testing, we need a class to test. Conveniently, there’s a class we can use in my svn repo: CU_Collection – a class which I wrote to demonstrate generic collections in PHP.
Here’s an example of how this class works:
//This collection only accepts strings $coll = new CU_Collection('string'); //Add a string and echo it $coll->add('This is a string'); echo $coll[0]; //This will throw an InvalidArgumentException because 1000 is not a string $coll->add(1000); |
This class isn’t very large, but it’s complex enough for us to demonstrate some testing principles on it. Now, let’s continue and start writing some tests!
Writing a test
Before we write the test, let’s remind ourselves of how the testcase should look:
<?php require_once '../../library/CU/Collection.php'; class CU_CollectionTest extends PHPUnit_Framework_TestCase { } |
This is the basic structure of the testcase class. Note that this time we have a require_once on top of the code. Why is this?
Because this code depends on the CU_Collection class, we actually need to include it to be able to test it. We are assuming that the test is located in a directory called “tests/CU/CollectionTest.php” under the main project directory, and the code for the CU_Collection class is in “library/CU/Collection.php”. You can structure your project differently if you wish.
There is also a way to use autoloading and include paths, but that will be discussed in a later post.
Now, let’s write that test. If you look at the code of the class, you can see it should start with an empty collection with no items in it. Let’s write a test for it:
<?php require_once '../../library/CU/Collection.php'; class CU_CollectionTest extends PHPUnit_Framework_TestCase { public function testCollectionStartsEmpty() { $coll = new CU_Collection('string'); $this->assertEquals(0, count($coll)); } } |
It may seem very simple at this point. Don’t worry, writing unit tests usually is this simple! Let’s look at some more examples next…
Testing code vs. testing behavior
As I mentioned in the introduction post, a good unit test checks the expected behavior of some code, not that the code does what it looks like it’s doing if you read it line by line.
Let’s write a test for the add method:
//this code again goes inside the CU_CollectionTest class public function testAdding() { $coll = new CU_Collection('string'); $coll->add('foobar'); $this->assertEquals(1, count($coll)); try { $coll->add(1); $this->fail('Should throw exception'); } catch(InvalidArgumentException $ex) { $this->assertEquals('Trying to add a value of wrong type', $ex->getMessage()); } } |
Is this a good test? It looks correct, and when you run it, it passes with flying colors. However, in a sense, this is not a good test!
The problem of this test is that it’s strictly written in the fashion “Okay, looks like the code does this, so let’s test that it does that”. While this test can be helpful, it would be better to rewrite it as we’ll see next. Also, consider the fact that the code you’re writing the test for could have a bug! If it was a more complex method than the one in this case, it could very well corrupt your test as a result if you don’t notice it.
Now, before writing the test, let’s consider what behaviors we expect from the add method:
- If we add a correct type, the length of the collection should increase
- If we add an incorrect type, we should get an InvalidArgumentException
So for this parts, the initial test was quite correct. It is good to keep in mind that you should always consider the behaviors, and only test one behavior per test, as we will do now. Sometimes you can see such behaviors from just the phpdoc comments without reading the code itself, but when writing tests for existing code you will often need to look at the code to find them.
First, let’s test the behavior #1:
public function testAddingCorrectType() { $coll = new CU_Collection('string'); $coll->add('foobar'); $this->assertEquals(1, count($coll)); } |
And the second behavior:
public function testAddingWrongTypeCausesException() { $coll = new CU_Collection('string'); try { $coll->add(1); $this->fail('Should throw exception'); } catch(InvalidArgumentException $ex) { } } |
Note here that the assertion which checked the exception’s message was removed. In this case, I think it’s enough that we test that the exception thrown is of the expected type. The content of the exception’s message itself is not so important.
Fixing bugs
Often you will want to write a unit test when you encounter a bug, to be sure that the bug will not reappear in the future. This is called regression testing, and I think it’s very important in longer running projects, or frameworks such as the Zend Framework.
This brings us to another thing that can help improve your tests:
A test should initially result in a failure.
This doesn’t mean you need to write an incorrect test, or intentionally break your code. It means that when writing a test for a bug, or when doing test-driven development, you should write your test before writing the actual code.
Let’s introduce a bug into the CU_Collection class so that we can demonstrate this.
Modify the remove method’s array_splice call to not define the amount of items to remove. An easy mistake to make, but it can be difficult to spot!
public function remove($index) { if($index >= $this->count()) throw new OutOfRangeException('Index out of range'); array_splice($this->_collection, $index); //Uh oh! } |
Remember that array_splice’s third parameter is the amount of items to remove, and if left undefined, it will remove everything in the array from that index onwards!
A naive unit test which checks only that remove works won’t do, such as this one:
public function testRemovingItem() { $coll = new CU_Collection('string'); $coll->add('foobar'); $coll->remove(0); $this->assertEquals(0, count($coll)); } |
Even though we introduced a bug, this one will still pass. Imagine if you fixed the bug, and then wrote this to test against it? Oops. Don’t get me wrong, this is an okay test, but it won’t help us this time.
We need a better test:
public function testRemovingItemFromTheMiddle() { $coll = new CU_Collection('string'); $coll->add('one'); $coll->add('two'); $coll->add('three'); $coll->remove(1); $this->assertEquals(2, count($coll)); } |
If we run the testcase against the broken code, this test will fail. Now we know that the test for our bug works, and we can then proceed to fix the bug. After fixing the bug, the test passes.
Conclusion
Now you should be able to write good tests yourself. Test behaviors, and fail first!
Do you have any burning questions? Feel free to any in the comments!
Next week in unit testing we’ll look into mock objects, which are necessary when testing more complex classes. Continue to Unit testing 4: Mock objects and testing code which uses the database