Simplify Your Tests With Custom Assertions

Did you ever have to write tests with quiet complex assertions? I mean when you need to perform some calculations to decide if actual value is ok. If so, you obviously thought that your test is looking kinda weird and heavy.

But there is a way to significantly improve readability of all tests, even the most complex and ugly ones.

Example problem

There is a bunch of algorithms, when you can not create example-based test, i.e. any algorithm using random generation. Our example will be about randomization too.

We have a randomly generated Basket of Balls. The Basket MUST meet the following conditions:

• It must contain \$length of balls;
• Every Ball must have unique number. So, there can be only one ball with given number in a Basket;
• Every Ball in a Basket must have a number within a given range, defined by \$min and \$max values.

The problem is, we cannot create any example-based test, because of random balls generation. There is another way: we execute test’s action (Basket generation) and check if those properties are met by actual result. We need to repeat this test sometimes to avoid lucky variations. This approach to testing is called property-based testing.

Let’s have a look on a test:

class BasketGeneratorTest extends \PHPUnit_Framework_TestCase
{
/** @var  array */
private \$config;

/** @var  int */
private \$length;

/** @var BasketGenerator */
private \$generator;

protected function setUp()
{
parent::setUp();

\$this->config = ['min' => 1, 'max' => 999];
\$this->length = 10;
\$this->generator = new BasketGenerator(\$this->config);
}

public function testGenerator()
{

\$this->assertCount(\$this->length, \$basket, 'Basket must be exactly given length.');

foreach (\$basket as \$ball) {
\$this->assertTrue(
\$ball->getNumber() <= \$this->config['max'] && \$ball->getNumber() >= \$this->config['min'],
\$message
);
}

for (\$i = 0; \$i < \$this->length; \$i++) {
for (\$j = \$i + 1; \$j < \$this->length; \$j++) {
if (\$balls[\$i]->equals(\$balls[\$j])) {
throw new \PHPUnit_Framework_AssertionFailedError(\$message);
}
}
}
}
}

On the following code snippets I wont’ include class definition and setUp() function to save a bit of place.

What we can say about this test at first sight? Assertions are very hard to understand. We need to read the whole block of code to understand what is actually checked by this test. Fortunately, there are plenty of ways to hide this complexity into easy-to-read assertions.

Naive Approach

The first idea is pretty simple: just define an assert function inside the test case:

public function testGenerator()
{

\$this->assertCount(\$this->length, \$basket, 'Basket must be exactly given length.');
\$this->assertUniqueBalls(\$basket, 'Basket must consist of unique balls.');
\$this->assertBallsRange(\$basket, 'Each Ball in generated Basket has number between configured borders.');
}

{
/** @var Ball \$ball */
foreach (\$basket as \$ball) {
\$this->assertTrue(
\$ball->getNumber() <= \$this->config['max'] && \$ball->getNumber() >= \$this->config['min'],
\$message
);
}
}

{

for (\$i = 0; \$i < \$this->length; \$i++) {
for (\$j = \$i + 1; \$j < \$this->length; \$j++) {
if (\$balls[\$i]->equals(\$balls[\$j])) {
throw new \PHPUnit_Framework_AssertionFailedError(\$message);
}
}
}
}

This approach improves test readability a lot, but you cannot reuse your custom assertions in other tests. If you need this, you may go further and convert these assertions to constraints.

Reusing Custom Assertions

Let’s extract the assertion to a separate class. PHPUnit has a mechanism of Constraints for this. You create a Constraint and assert that a value matches this Constraint.

<?php // tests/Constraint/UniqueBalls.php
namespace Constraint;

class UniqueBalls extends \PHPUnit_Framework_Constraint {
/**
* This function must return true if given value matches the constraint
* @param Ball[] \$balls
* @return boolean
*/
public function matches (\$balls) {
\$length = count(\$balls);
for (\$i = 0; \$i < \$length; \$i++) {
for (\$j = \$i + 1; \$j < \$length; \$j++) {
if (\$balls[\$i]->equals(\$balls[\$j])) {
return false;
}
}
}

return true;
}
}

Then you have two options: to use the constraint directly in assertThat call or create a base test class with assertion functions:

assertThat example

public function testGenerator()
{

\$this->assertCount(\$this->length, \$basket, 'Basket must be exactly given length.');
\$this->assertThat(\$basket->getBalls(), new Constraint\UniqueBalls(), 'Basket must consist of unique balls.');
}

Base test case

<?php // tests/BaseTestCase.php
class BaseTestCase extends \PHPUnit_Framework_TestCase
{
public static function assertUniqueBalls(array \$balls, \$message = '') {
self::assertThat(\$basket->getBalls(), new Constraint\UniqueBalls(), \$message);
}
}