Saturday, July 10, 2010

PHP Application Lifecycle: Unit Testing

The concept of unit testing is nothing new, but unfortunately it seems to still be rare among PHP developers. I believe it's not because developers don't think testing is a good idea, but instead that they think testing is hard, or makes development times longer. I actually used to be one of those developers.

I think that most developers would agree that testing is a good thing, and we should all be doing it. Some developers like to test their work simply by calling it in another bit of code (or reloading the page in browser) and observing the results. The problem with this approach is that it is inflexible. While it may work fine on smaller bits of code, larger classes and objects that interact with other objects may not be as easy to test. Most of the time, these quick one-off tests assume perfect conditions, which isn't always the case.

By using a testing framework, a developer can quickly build test cases for a bit of code, and run those tests as they continue to make changes to the code in order to make sure nothing gets broken. In fact, this type of testing is called regression testing, and is only one type of test a developer can create. The most common types of tests are:
Smoke Test
The first, simple test against a new bit of code. These are used to check the code for expected behavior with valid input.
Regression Test
A set of tests written to verify and fix specific bugs or usage scenarios. For example, if a method expects a string, and causes a bug if given an integer, a regression test should be written - which fails - to verify the presence of the problem. The code should then be fixed to make the test pass. Regression tests are then used to make sure a bug is not re-introduced in future code revisions.
Integration Test
More advanced testing which checks the interaction between two or more portions of code. An integration test might be written to make sure a library is properly writing data to the database.
Behavior Test
Another more advanced testing methodology in which the test isn't concerned so much with the result, but how the code works internally. If a bit of code is expected to log data to a file, a behavior test will call that bit of code, and watch for the proper call to the log method.

PHP has two main unit testing tools: SimpleTest by Marcus Baker, and PHPUnit by Sebastian Bergmann. SimpleTest's Website hasn't been updated in a while, and I'm not sure of the state of the tool. PHPUnit is the most widely accepted, and is compatible with the xUnit family of testing tools. I use and will focus on PHPUnit for this discussion. PHPUnit supports all the test types outlined above, but for brevity I'm only going to review a simple smoke test.

Let's assume a simple class which provides a few math-based methods. It may look like this:
<?php
class Calculator
{

public function add($first, $second)
{
return (int) $first + (int) $second;
}

public function subtract($first, $second)
{
return (int) $first - (int) $second;
}

public function multiply($first, $second)
{
return (int) $first * (int) $second;
}

public function divide($first, $second)
{
return (int) $first / (int) $second;
}

}

A quick one-off test for this may look like this:
<?php
$calc = new Calculator;

echo "add(): ";
// Should output "4"
echo $calc->add(2, 2);
echo PHP_EOL;

echo "subtract(): ";
// Should output "2"
echo $calc->subtract(4, 2);
echo PHP_EOL;

echo "multiply(): ";
// Should output "10"
echo $calc->multiply(5, 2);
echo PHP_EOL;

echo "divide(): ";
// Should output "5"
echo $calc->divide(10, 2);
echo PHP_EOL;

Output would look like this:
[rchouinard@beta ~]$ php testCalc.php
add(): 4
subtract(): 2
multiply(): 10
divide(): 5

This approach seems simple, but some problems become apparent as development on the Calculator class continues. For starters, the test script doesn't really indicate what the test is checking for. The person invoking the script must know what output is expected in order to tell if the test passed or failed. This test script can be rewritten as a PHPUnit test case very easily:
<?php
require_once 'Calculator.php';
require_once 'PHPUnit\Framework\TestCase.php';

class CalculatorTest extends PHPUnit_Framework_TestCase
{

private $calc;

protected function setUp ()
{
parent::setUp();
$this->calc = new Calculator;
}

protected function tearDown ()
{
$this->calc = null;
parent::tearDown();
}

public function testAdd ()
{
$this->assertEquals(4, $this->calc->add(2, 2));
}

public function testSubtract ()
{
$this->assertEquals(2, $this->calc->subtract(4, 2));
}

public function testMultiply ()
{
$this->assertEquals(10, $this->calc->multiply(5, 2));
}

public function testDivide ()
{
$this->assertEquals(5, $this->calc->divide(10, 2));
}
}

Running PHPUnit against this file gives us easy to read and understand output:
[rchouinard@beta ~]$ phpunit CalculatorTest.php
PHPUnit 3.5.0beta1 by Sebastian Bergmann.

....

Time: 0 second, Memory: 1.00Mb

OK (4 tests, 4 assertions)

If one of the assertions had failed, we would get output like this:
[rchouinard@beta ~]$ phpunit CalculatorTest.php
PHPUnit 3.5.0beta1 by Sebastian Bergmann.

.F..

Time: 0 seconds, Memory: 1.00Mb

There was 1 failure:

1) Calculator::testSubtract
Failed asserting that matches expected .

/home/rchouinard/working/CalculatorTest.php:29

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.

Hopefully some of the benefits of a testing framework are apparent now. Our test code doesn't have to deal with output, and we have immediate pass/fail feedback without having to know what values the test is expecting. PHPUnit even tells us exactly what went wrong and caused the test to fail.

This has been a very simple intro to PHPUnit, and doesn't even begin to scratch the surface of what PHPUnit is capable of. I would encourage you to take a look at the PHPUnit documentation to learn more. For a working example of a PHPUnit setup, take a look at my PHP component library.

In coming posts, I'll discuss integrating PHPUnit into other tools for some truly powerful code analysis.

3 comments:

  1. Yo! Nice intro to testing. I definitely think that getting in to the habit of writing tests is a bit rough and could add to dev time. Once it becomes second nature its smooth sailing.

    I've been trying to do significantly more TDD lately for a newer project. I really like how it forces you to think about how you want to use your code.

    ReplyDelete
  2. This has been a very simple intro to PHPUnit, and doesn't even begin to scratch the surface of what PHPUnit is capable of.

    Pgp software

    ReplyDelete