PHPUnit: Writing a Custom Assertion

Posted on by Matthias Noback

When you see yourself repeating a number of assertions in your unit tests, or you have to think hard each time you make some kind of assertion, it's time to create your own assertions, which wraps these complicated assertions into one single method call on your TestCase class. In the example below, I will create a custom assertion which would recognize the following JSON string as a "successful response":

{"success":true}

Inside a TestCase we could run the following lines of code to verify the successfulness of the given JSON response string:

$resonse = $someProbablyMockedWebservice->call();

$response = json_decode($response, true);
$this->assertInternalType('array', $response);
$this->assertNotSame(null, $response);
$this->assertArrayHasKey('success', $response);
$this->assertTrue($response['success']);

Which means I convert the string I received into an array using json_decode(), then I check for a key named "success" and assert it's value is true (which means, "I am a successful response").

Create a custom constraint

According to the PHPUnit documentation I should extend \PHPUnit_Framework_Constraint. That's right, but it also tells me to override the evaluate() method, which is not really the most elegant way to accomplish what we want. In fact, the original evaluate() method has all we need and as we take a look at what happens inside, it appears it calls another method called match() to see whether or not it should mark itself as "failed":

abstract class PHPUnit_Framework_Constraint implements Countable, PHPUnit_Framework_SelfDescribing
{
    // ...

    public function evaluate($other, $description = '', $returnResult = FALSE)
    {
        $success = FALSE;

        if ($this->matches($other)) {
            $success = TRUE;
        }

        if ($returnResult) {
            return $success;
        }

        if (!$success) {
            $this->fail($other, $description);
        }
    }
}

So, let's implement the matches() method. This method should return false upon failing to verify that the actual value matches some abstract expected value. In the example of asserting that a string represents a successful JSON response, we should add a few checks and return true if all failing options are checked:

namespace Matthias\PHPUnit;

class IsSuccessfulJsonResponseConstraint extends \PHPUnit_Framework_Constraint
{
    public function matches($other)
    {
        if (is_string($other)) {
            $other = json_decode($other, true);

            if (null === $other) {
                return false;
            }
        }

        if (!is_array($other)) {
            return false;
        }

        if (!array_key_exists('success', $other)) {
            return false;
        }

        if (true !== $other['success']) {
            return false;
        }

        return true;
    }
}

Furthermore, we need to add a string which PHPUnit uses for rendering a message in case the matches() method returns false:

class IsSuccessfulJsonResponseConstraint extends \PHPUnit_Framework_Constraint
{
    // ...

    public function toString()
    {
        return 'is a successful JSON reponse';
    }
}

Putting our custom constraint into use

A constraint is only one part of an assertion. It is used as the second argument of a method called assertThat() which is available inside every TestCase. When we want to use our custom constraint, we can call it like this:

class WebserviceTest extends \PHPUnit_Framework_TestCase
{
    public function testCall()
    {
        $response = $someWebservice->call();
        self::assertThat($response, new \Matthias\PHPUnit\IsSuccessfulJsonResponseConstraint());
    }
}

Adding a assertIsSuccessfulResponse() method

It would be much nicer (and "D.R.Y.-er") to put this statement inside a single method, like this:

class WebserviceTest extends \PHPUnit_Framework_TestCase
{
    public static function assertIsSuccessfulJsonResponse($response, $message = '')
    {
        self::assertThat($response, new \Matthias\PHPUnit\IsSuccessfulJsonResponseConstraint(), $message);
    }
}

Or as the PHPUnit documentation suggests it (for better readabilty I think):

class WebserviceTest extends \PHPUnit_Framework_TestCase
{
    public static function assertIsSuccessfulJsonResponse($response, $message = '')
    {
        self::assertThat($response, self::isSuccessfulJsonResponse(), $message);
    }

    public static function isSuccessfulJsonResponse()
    {
        return new \Matthias\PHPUnit\IsSuccessfulJsonResponseConstraint();
    }
}

Now you can assert the successfulness of the response string like this:

class WebserviceTest extends \PHPUnit_Framework_TestCase
{
    public function testCall()
    {
        $response = $someWebservice->call();
        $this->assertIsSuccessfulJsonResponse($response);
    }
}

Of course, you can also add methods like this in your own base TestCase class and extend your TestCase classes from them.

Unit testing your custom constraint

This last step may be easy to forget: you must also unit test the constraint you have just created! Especially since you are going to use this piece of code to assert the correctness of other pieces of your code. So add some tests like these:

namespace Matthias\Tests\PHPUnit;

class IsSuccessfulJsonResponseConstraintTest extends \PHPUnit_Framework_TestCase
{
    private $constraint;

    public function setUp()
    {
        $this->constraint = new \Matthias\PHPUnit\IsSuccessfulJsonResponseConstraint();
    }

    public function testValidJsonEncodedString()
    {
        $this->assertFalse($this->constraint->matches('invalid JSON'));
    }

    public function testPropertySuccessShouldExist()
    {
        $this->assertFalse($this->constraint->matches('{"some-property":"some-value"}'));
    }

    public function testPropertySuccessShouldBeTrue()
    {
        $this->assertFalse($this->constraint->matches('{"success":false}'));
    }

    public function testSuccessFuJsonlResponse()
    {
        $this->assertTrue($this->constraint->matches('{"success":true}'));
    }
}
PHP Testing PHPUnit assertion
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Jesús Franco

Thanks a lot, I'm working my way through learning TDD and it's awesome to see even the helpers need to be tested before being used in production.

Matthias Noback

That's great to hear, thanks, and good luck!

Anthony

Thank you, I was exacly looking for thar :)

hlegius

This is exactly what I was looking for in days!

Thanks for sharing!