Wednesday, May 23, 2007

Mock Database objects, in pearweb

Some things to get you started. pearweb is a pear installable version of the pear website and package management tools.

It would be fair to say it's a little bit... old. However, there are signs of life in the old beast yet, with a slow but sure unit testing suite being built up.

I'm a phpunit junkie, but I haven't used half of the features in it. As betrand points out, there's a lot of complexity under the hood of phpunit.

It's probably for that reason that pearweb have rolled their own very simple unit testing framework. It provides all of your basics; assertTrue, assertFileExists, and what have you. It's called PEAR_PHPTest.

That isn't what I want to talk about, though. I want to talk about the implementation of mock database objects in pearweb, for unit testing purposes.

So: a unit test isn't a unit test if it relies on a database. At work, we usually don't care, and just expect a database to be there. pearweb is a little bit more strict.

So pearweb uses PEAR::DB, and to implement a mock database object, they have created a mock db driver.

To use it is pretty simple, if a little tedious. I'm sure some kind of code generator helper would save heaps of time here.

All you do is:
$mock->addDataQuery("SELECT * FROM categories ORDER BY name",
array(array('id' => 1,
'parent' => null,
'name' => 'test',
'summary' => null,
'description' => 'hi there',
'npackages' => 0,
'pkg_left' => 0,
'pkg_right' => 0,
'cat_left' => 1,
'cat_right' => 2)),
array('id', 'parent', 'name', 'summary', 'description', 'npackages', 'pkg_left',
'pkg_right', 'cat_left', 'cat_right'));


The first parameter is obviously the sql you are executing, the second is an array of result arrays, the third is the columns you expect in the result.

This means you can freeze the exact state of an object in the database, and run unit tests against it, without having to hit the database. Say bye bye to slow tests!


There are methods to simulate updating, inserting, and deleting too.

$sql = "INSERT INTO notes (id,uid,nby,ntime,note) VALUES(%s,%s,'%s','%s','%s')";
$sql = sprintf($sql, $data['id'], $data['uid'], $data['nby'], $data['ntime'], $data['note']);
$mock->addInsertQuery($sql, array(), 1);


So what, you say. That's pretty pointless, you say. Well, not if you expect a sequence of database queries to happen, and happen exactly how you want them to.

For that, we have the queries property of a mock db object.


$db = DB::factory('mock');

$myObject = new MyObject();
$myObject->removeLosers();

$phpunit->assertTrue($db->queries ==
array("SELECT id FROM losers",
"DELETE FROM users");


With a little bit of help from a code generator, there's a definite potential to never be in the dark about what queries were executed again.

Neat, hey. In order to bring some of this goodness to the rest of you, I've logged #11107: Add a PEAR::DB driver for mock objects and #11108: add a PEAR::MDB2 driver for mock objects.

Even if neither of those bugs get implemented, you can always grab the code from CVS

No comments: