Irrational Exuberance!

Simple Unittesting in PHP

February 1, 2009. Filed under testingphp

If Perl is the Swiss Army chainsaw of the internet, PHP might be the dull butterknife from that silverware set your Aunt Girda gave you because she felt guilty about throwing it away. Sure, you're never going to cut down that redwood, but when you're cutting butter, butterknives have good points too. Today's butter is unittesting for small PHP projects.

Setting up a light unittesting setup is pretty trivial as long as your language supports using functions as parameters. Which, fortunately, PHP does. For example, a none-too-impressive implementation of map could be written like this:

<?php
function my_echo($x) {
    echo "my_echo: $x";
}

function map($func, $lst) {
  $mapped = array();
  foreach($lst as $item) {
    $mapped[] = $func($item);
  }
  return $mapped;
}

map("my_echo", array('a','b','c'));
?>

With that in mind, let's consider what basic unittesting consists of:

  • Comparing actual and expected values.
  • Defining testcases.
  • Running testcases.
  • Reporting results.

With an emphasis on basic, we can get all of that for remarkably cheap:

<?php
$verbose = $_GET['verbose'];
$pass = 0;
$fail = 0;

function assert_($a, $b) {
  if ($a != $b) {
    echo "<p class=\"error\">error: $a != $b<?p>";
    global $fail;
    $fail += 1;
  } else {
    global $pass;
    $pass += 1;
    echo '<span>.</span>';
  }
}

function test_x() {
  assert_(1,2);
  assert_("yes","yes");
}

function test_y() {
  assert_(count(array(1,2,3)), strlen("abc"));
}

$tests = array('text_x','text_y');

foreach($tests as $test) {
  set_up();
  if ($verbose) { echo "<p>$test</p>"; }
  $test();
  tear_down();
}

echo "<p>Passed $pass tests. Failed $fail tests.</p>";
?>

If you're bothered by the reliance on global state, or that it would be hard to extend, we improve upon it a bit by using PHP5 style OO (at a loss of simplicity).

<?php
class SimpleTest {
  public $passed;
  public $failed;
  public $current;
  public $msgs;

  public function __construct() {
    $this->passed = 0;
    $this->failed = 0;
    $this->msgs = array();
  }

  public function assert($a,$b) {
    if ($a == $b) {
      $this->passed++;
      echo '<span>.</span>';
    } else {
      $this->failed++;
      echo '<span>X</span>';
      $this->msgs[] = "$this->current: $a != $b";
    }
  }

  public function run_tests($tests) {
    foreach($tests as $test) {
      $this->current = $test;
      $test($this);
    }
    foreach($msgs as $msg) {
      echo "<p>$msg</p>";
    }
    echo "<p>$this->passed passed tests, and $this->failed failed tests.</p>";
  }
}

function test_math($st) {
  $st->assert(1*2,2);
  $st->assert(5+0,5);
}

function test_strings($st) {
  $st->assert("test","test");
  $st->assert(strlen("test"),4);
}

$st = new SimpleTest();
$st->run_tests(array("test_math", "test_strings");
?>

If you're in the mood for some overengineering, it's a pretty short leap from here to a flexible and extensible unittesting framework. For a start, we can rewrite the run_tests method to look through all user defined functions for those that begin with test_ and then run those tests.

<?php
public function run_tests() {
    $all_funcs = get_defined_functions();
    $user_funcs = $all_funcs['user'];
    foreach($user_funcs as $func) {
      if (strpos($func, "test_") === 0) {
        $this->current = $func;
        $func($this);
      }
    }
}
?>

In either case, running the tests is as simple as reloading the page (or could be done at the command line), and doing this exercise has firmly dragged PHP from my mental junk bin (into some kind of halfway home for programming languages). The equivalent code written in Python strikes me as much more pleasant, but the difference isn't as overwhelming as I might have once imagined.