Many thanks to everyone who attended my Test Driven Development tutorial today at php|tek in Chicago. As promised, here is the code we developed during the live coding sections of the tutorial.
The slides I presented are available from the php|tek site here.
Our first example was a simple Hash object. The requirements we developed for the hash were:
***Hash to repond to get(key) and set(key, value)
***Hash to have isValid(key)
***load from associative array at construction
access via object notation $hash->key or $hash->key = value
***access via array notation $hash[key] or $hash[key] = value
During our session, we covered 4 out of the 5 requirements using TDD.
Here was the test case we ended up with for our Hash object:
<?php
class HashTestCase extends BaseTestCase {
function testClass() {
$this->assertTrue(class_exists('Hash'));
}
function testGetAndSet() {
$hash = new Hash;
$this->assertMethodExists($hash, 'get');
$this->assertMethodExists($hash, 'set');
$hash->set('foo', 'bar');
$this->assertEqual('bar', $hash->get('foo'));
$hash->set('baz', 'blah');
$this->assertEqual('blah', $hash->get('baz'));
}
function testIsValid() {
$hash = new Hash;
$this->assertMethodExists($hash, 'isValid');
$this->assertFalse($hash->isValid('foo'));
$hash->set('foo', 'bar');
$this->assertTrue($hash->isValid('foo'));
}
function testLoadDuringConstruction() {
$arr = array('foo' => 'bar');
$hash = new Hash($arr);
$this->assertEqual('bar', $hash->get('foo'));
$hash2 = new Hash('false');
$hash2->set('foo','bar');
$this->assertEqual('bar', $hash2->get('foo'));
}
function testAccessAsArray() {
$hash = new Hash(array('foo' => 'bar'));
$this->assertEqual('bar', $hash['foo']);
}
}
?>
The code we developed for the Hash class was:
<?php
class Hash implements ArrayAccess {
function __construct($vals = false) {
$this->vals = (is_array($vals)) ? $vals : array();
}
protected $vals = array();
function get($key) {
return $this->vals[$key];
}
function set($key, $value) {
$this->vals[$key] = $value;
}
function isValid($key) {
return array_key_exists($key, $this->vals);
}
// functions to implement ArrayAccess
function offsetExists($key) {
return $this->isValid($key);
}
function offsetGet($key) {
return $this->get($key);
}
function offsetSet($key, $val) {
$this->set($key, $val);
}
function offsetUnset($key) {
unset($this->vals[$key]);
}
}
?>
After we reviewed more of the benefits of using TDD, we move on to a more meaty application: a guest book. Here was the test cases we developed:
<?php
class BaseTestCase extends UnitTestCase {
function assertMethodExists($object, $method) {
$this->assertTrue(method_exists($object, $method), get_class($object).' object has '.$method.' method');
}
}
class ModelTestCase extends BaseTestCase {
function testClass() {
$this->assertTrue(class_exists('Guestbook'), 'Guestbook class exists');
}
function testSetDb() {
$model = new Guestbook;
$this->assertMethodExists($model, 'setDb');
}
function testGetComments() {
$model = new Guestbook;
$this->assertMethodExists($model, 'getComments');
$this->assertIsA($comments = $model->getComments(), 'array'
, 'Guestbook::getComments() returns an array [%s]');
$this->assertTrue(count($comments)>1, 'returned more than one comment');
$this->assertGreaterThan(
$comments[0]['created']
,$comments[1]['created']
);
}
function testGetCommentQueryDatabaseWithOrderByClause() {
$model = new Guestbook;
$db = new Mockadodb_mysql;
$db->expectOnce('getArray', array(new WantedPatternExpectation('/order\s+by\screated\s+desc\s*$/i')));
$model->setDb($db);
$model->getComments();
}
function assertGreaterThan($val1, $val2) {
$this->assertTrue($val1 > $val2, $val1.' is greater than '.$val2);
}
}
class DbTestCase extends BaseTestCase {
function testDbConnection() {
$this->assertIsA($db = DB::conn(), 'adodb_mysql');
$this->assertMethodExists($db, 'getArray');
}
function testAccessToGuestbookTable() {
$db = DB::conn();
$this->assertIsA($rs = $db->getArray('select * from Guestbook'), 'array');
$this->assertTrue(count($rs) > 1, 'result contains more than one row');
$expected_keys = array('name','comment','created');
foreach(array_keys($rs[0]) as $key) {
$this->assertTrue(in_array($key,$expected_keys), $key.' was not expected');
}
}
}
class PageTestCase extends WebTestCase {
protected $url='http://gentoo/~sweatje/conf/phpt_tdd/live/guestbook.php';
function testPage() {
$this->get($this->url);
$this->assertNoUnwantedPattern('/fatal error/i');
$this->assertTitle('Live Guestbook');
$this->assertWantedPattern(
'~
(<h2>\s*.*?</h2>.*?\w+.*?.*){2,}
~imsx');
}
function testInputForm() {
$this->get($this->url);
$this->assertFieldByName('name');
$this->assertFieldByName('comment');
}
}
With these test cases, we developed:
db.php:
?>
<?php
require_once 'adodb/adodb.inc.php';
class DB {
//static class, we do not need a constructor
private function __construct() {}
public static function conn() {
static $conn;
if (!$conn) {
$conn = adoNewConnection('mysql');
$conn->connect('localhost', 'phpauser', 'phpapass', 'phpa');
$conn->setFetchMode(ADODB_FETCH_ASSOC);
}
return $conn;
}
}
?>
model.php
<?php
/**
getComments() sorted by most recent first
listGuestNames()
addComment() - no blank comment, no SPAM
*/
require_once 'db.php';
class Guestbook {
protected $db;
function __construct() {
$this->db = DB::conn();
}
function setDb($db) {
$this->db = $db;
}
function getComments() {
return $this->db->getArray(
'select
*
from Guestbook
order by created desc');
}
}
?>
guestbook.php
<?php
<html>
<head>
<title>Live Guestbook</title>
</head>
<body>
<h1>All my comments</h1>
< ?php
require_once 'model.php';
$guestbook = new Guestbook;
foreach($guestbook->getComments() as $comment) {
echo '<h2>', $comment['name'], '</h2>', $comment['comment'], '';
}
?>
<h1>Add your comment</h1>
<form method="post">
<input type="text" name="name"/>
<textarea name="comment"></textarea>
</form>
</body>
</html>
?>
tek07