Saturday, February 20, 2010

It's not a question whether you should unit test but how much!

Introduction


Unit testing matters because it will make your code more reliable. This is especially true when code needs to be updated as time goes along. Unit testing takes discipline. It is tempting to just hack a result together and use ad-hoc testing. However, by recording these ad-hoc tests in actual unit tests you will be able to run them over and over again. It is not a question of whether you should do unit testing. Rather the question is: "What's the level of detail you are going to do unit testing?". Keep it basic, especially in the beginning. Unit tests are great for diagnostics and make your programming efforts more sustainable in the long run.

From personal experience, I can tell you how many times I have quickly build/hacked applications together. Weeks later, when I had to change something basic, this could take hours or even a day because the change I made caused other parts to fail and it was hard to diagnose. Unit testing is a skill that can help you get to the next level as a programmer as well as an organization. It will take effort and persistence but when done in the right amount you should be able to reap benefits in weeks.

RawDev has tried to make unit testing as streamlined and easy to do as possible. There are 3 aspects to unit testing in RawDev: (1) a unit test, (2) a component test (the test classes that you will write), and (3) the unit test binary that runs all, some or one unit test.

Unit Test


Overview

Code is organized in functions. These functions reside in objects or are static class functions. In addition, functions can interact with their environment.



Function
Usually an object function or a static class function. Functions can interact with their environment.
Input A function has zero or more input parameters. In rare cases these parameters defined as references in which case they can be altered by the function.
Output A function has one output parameter which could be an array.
Exception Usually based on incorrect input or failing of a resource, an exception can be thrown, which will interrup the regular flow of the code.
Object & global state Object functions can modify the state of object variables. In addition, any function can modify class variables and other global state such as databases, files etc.

Example 1


A unit test typically tests either correct output of a function or verifies if the expected exception was raised. In addition one or more asserts can be done after the function was executed. Asserts are typically tests of the object and global state. Don't worry about how results of tests are displayed, I will get to that in the next section. RawDev uses fluid API calls for usability.

Let's look at some examples. First a real basic example of a function that calculates the sum of 2 or more inputs. Each test has a title for diagnostics but as we will see later the title will set to the test function that implements the test.

<?php

require_once('rawdev/RawDev.php');
require_once(RAWDEV_LIB.'/Test/Unit/Test.php');

function sum() {
  $params = func_get_args();
  if (count($params) < 2) throw new RException('math_too_few_params', 'Two or more parameters please.');    
  $sum = 0;   
  foreach($params as $param) $sum += $param;    
  return $sum; 
}

# test 1 
$t = RUnitTest::construct('sum')->title('Two Params')->in(1, 2)->out(3)->test();
print $t->icon;

# test 2
$t = RUnitTest::construct('sum')->title('One Params')->in(1)->exception('math_too_few_params')->test();
print "$t->icon\n";

?>

The steps of the first test are: set function, set title, set input params, set output params, test function and evaluate results.

The second function tests wheter the exception is thrown as expected.

Example 2


Let's look at a simple object test next.

<?php

require_once('rawdev/RawDev.php');
require_once(RAWDEV_LIB.'/Test/Unit/Test.php');

class Car {

var $miles = 0;

function drive($miles) {
if ($miles <= 0) throw new RException('car_illegal_miles', 'Miles should be more than zero.'); $this->miles += $miles;
}

}

$c = new Car();
$f = array($c, 'drive');

$t = RUnitTest::construct($f)->title('Simple Drive')->in(10)->out()->test();
$t->assert(10, $c->miles);
print "$t->icon\n";

?>

In this example the test verifies whether the output is NULL. The assert tests wheter the "object variable" mile was set to 10.

Component Test


The three components to test in RawDev are: modules (or sub-modules), classes and functions. Each of these components has a setup routine (optional), zero or more unit tests and a cleanup routine (optional). Each Component test is implemented as a test class. Creating test classes for modules and classes is often not necessary.

Example


The example below shows all the tests for the "drive" function (see example above). Note that the unit function creates a unit test object and sets the title to the name of the function. All functions that start with test[Name] are run as unit tests. It is perfectly ok to create helper functions that do not start with "test".

<?php

require_once('rawdev/RawDev.php');
require_once(RAWDEV_LIB.'/Test/Unit/Component.php');

# class from the previous example
class Car {

var $miles = 0;

function drive($miles) {
  if ($miles <= 0) throw new RException('car_illegal_miles', 'Miles should be more than zero.'); 
  $this->miles += $miles;
}

}

# this shows what your test harnass will look like
class TCarDrive extends RComponentUnitTest {

  var $car;
  var $f;

function setup() {
  $this->car = new Car();
  $this->f = array($this->car, 'drive');
}

#Drive10 will show up as the title of the unit test!
function testDrive10() {
  $t = $this->unit($this->f)->in(10)->out()->test();
  $t->assert(10, $this->car->miles);
}

#Negative: test for an exception
function testNegative() {
  $t = $this->unit($this->f)->in(-1)->exception('car_illegal_miles')->test();
}

}

#run the harnass, in real life this is done throug: bin/runit.php
$c = new TCarDrive('function');
$c->test();
$c->display();


?>

Component Test location


Test classes are saved in the "tests" directory using a specific path/name structure:

[RAWDEV_HOME]/tests/Util.php : indicates a component test for module Util.
[RAWDEV_HOME]/tests/Util/Car.php : indicates a component test for class Car (part of the Util module)
[RAWDEV_HOME]/tests/Util/Car/drive.php : indicates a component test for function drive of class Car (part of the Util module)

Module and Class Components

Module and class components are not required. In addition, they can contain setup/cleanup routines for the module or class only. Modules and classes do not have dependencies because they are expected to be tested as independent modular constructs.

Dependencies

A function may have implicit or explicit dependencies on other functions in the class. RawDev detects implicit dependencies by reading the source of the function. Explicit dependencies are added using the @dependencies notation in the function comment header (e.g. @dependencies __construct,start). RawDev (a) automatically rearranges the order that functions are tested based on these dependencies and (b) will unit test dependent functions if a depending function is tested in isolation.

Unit Test binary


[RAWDEV_HOME]/bin/runit.php is the binary that runs all unit tests it accepts zero or one argument which is the path of the component to be tested.

Examples:

run all unit tests:
[RAWDEV_HOME]/bin/runit.php

run all unit tests for module Util:
[RAWDEV_HOME]/bin/runit.php Util

run all unit tests for class Car:
[RAWDEV_HOME]/bin/runit.php Util/Car

run all unit tests for function drive:
[RAWDEV_HOME]/bin/runit.php Util/Car/drive

run one unit test named drive10
[RAWDEV_HOME]/bin/runit.php Util/Car/drive/testDrive10

One last note that when a component is tested, then all it's child components are tested as well. It's parent components are not tested. However the setup and cleanup routines are called for all it's parents.

Test Output


The output of any component test (or all tests) is divided into 3 parts: tree view, overall view, and failed test messages.

Example:
php runit.php Util

Util 
    Dumper 
        display .....
        dump ..
        formatScalar .......
        maxLength ..
        page ....
    Match 
        __construct .
        _match ...
        matchObject ...
        matchScalar .....
        matchArray .......
    Stdin ..
    Util 
        max .X.
        min ...
        nvl .......
        preg ................
        writeFile ..

..........................................X.......
......................

X: Util/Util/max:Array                     : Value [9] does not match expected value [10] for item []. in Match.php (115)
   Match.php            96
   Match.php            57
   Test.php             179

As you can see all tests succeeded except for the "testArray" unit test function of function max in the Util class. Extra information is displayed (including a simple trace) why the test failed.

Statuses:
. : OK
X : FAIL (output does not match expected)
E : EXCEPTION (unexpected exception occured)
? : NO_EXCEPTION (expected exception did not occur)

Summary


Unit Testing takes discipline. When done at the appropriate amount it will increase productivity and reliability. It is also a great diagnostic tool.

RawDev organizes unit tests into component test classes. The runit binary can test one unit test, all unit tests or any component and it's children. When a function changes the state of the object or global state then multiple assert calls can be made to verify the changed state. For this reason the object or global variables should be accessible publicly or by method. Unit tests are most effective when functions have one specific task. Doing unit testing will actually alter the way you code (this is not a commercial for suits).

Links

RUnitTest API Doc

RComponentUnitTest API Doc

PHP Magazine articel

No comments:

Post a Comment