Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 638 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Testing Patterns: Delta Assertion

02.07.2011
| 5148 views |
  • submit to reddit

Sometimes we do not know the initial state of the fixture we are going to use for a test. In this case, it's likely to be a Shared Fixture, or even simply a Standard one which we do not want to be coupled to.

In other, more complex scenarios, the fixture is non-deterministic as it has been used by previous tests, or it is generated almost randomly.

As en example, consider a database that is being filled and truncated by previous tests. Recreating it from scratch at the start of each test would be too slow. But we can't define a simple assertion, as it would depend on the initial conditions.

Instead, we write a Delta Assertion, which calculates the changes in the state of the SUT between the test start and the assert phase. It is this delta that is subjected to checks, instead of the whole final state. In mathematics, delta is a common term for indicating a change in a variable's value

The result is that you are less dependent on the data contained in the fixture: your tests are less fragile. When using Standard Fixtures instead of Minimal ones, this pattern is particularly handy as it prevents a bunch of your tests from failing just because a colleague has updated the fixture to add other tests.

In our example, we will consider testing a UserDao (or UserRepository, it's the same), that hides a relational table where users are stored. This are the phases of such a test if it employs a Delta Assertion:

  • arrange: you count N rows in the corresponding User database table. This number is saved in a local variable, or in some data structure.
  • act: you add a User to the database. This is quite normal.
  • assert: you check that there are N+1 rows in the table.

Implementation

In PHP we have usually no concurrency problems in running Test Suites, unless you are running more than one at the same time. Delta Assertions do not solve these issues automatically anyway. However, the state shared between tests shouldn't even be visible by different PHP processes.

A caveat of this pattern is that the snapshot of the initial state should not be touched by the SUT. If it consists of values (scalars), like a count() done in our example, you're safe.

But when using objects, sometimes it's useful to clone them or perform some sort of copy of their relevant fields. For example, I remember assertions made over a Query object fail because the SUT modified the Query passed in that, and the original Query object stored in a variable was actually the same object: there were no differences between the starting state and the final one.

Thus keep in mind that Delta Assertions are a clever solution to a problem that most of the time you don't have: if you isolate your objects well, you'll know their initial state and their final one deterministically.

Example

As I've said earlier, this example shows how to test a UserDao with a in-memory database. We do not make assumptions on the content of the users table, so that if you want to move the setUp() in your bootstrap or somewhere else, the test would still pass.

<?php
class DeltaAssertionTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
// imagine that this fixture is a Shared one
// we wouldn't know the particular state of the table
$this->connection = new PDO('sqlite::memory:');
$this->connection->exec('CREATE TABLE users (nickname VARCHAR(255) NOT NULL PRIMARY KEY, password VARCHAR(255))');
}

public function testAnUserIsAdded()
{
$userDao = new UserDao($this->connection);
$previous = $userDao->countUsers();

// if the nickname is not unique, however, the test would still fail
// but we are covered against all other records inserted in the table
$userDao->addUser('aUniqueNickname', 'password');

$this->assertEquals($previous + 1, $userDao->countUsers());
}
}

class UserDao
{
public function __construct(PDO $connection)
{
$this->connection = $connection;
}

public function countUsers()
{
$stmt = $this->connection->query('SELECT COUNT(*) FROM users');
$result = $stmt->fetchAll();
return $result[0][0];
}

public function addUser($nickname, $password)
{
$stmt = $this->connection->prepare('INSERT INTO users (nickname, password) VALUES (:nickname, :password)');
$stmt->bindValue('nickname', $nickname, PDO::PARAM_STR);
$stmt->bindValue('password', $password, PDO::PARAM_STR);
$stmt->execute();
}
}
Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)