How to test custom PHPUnit assertions & helpers

Published January 11, 2022; last updated on January 12, 2022
How to test custom PHPUnit assertions & helpers

When building applications, a decent testsuite is an absolute must-have. In the short run it might sound like a waste of time (“I’m sure I implemented this feature correctly”), but one year later you’ll not be so sure anymore when you need to make a few changes.

To make testing somewhat more pleasant, you can write your own custom testing helper functions that make your tests more expressive and easy-to-read. In this article, I’ll show you how to test your own testing helpers/assertions with PHPUnit.

Custom PHPUnit assertions

In order to start testing our own helpers, we’ll first need a test. Let’s start with this example test, that asserts whether a string is a greeting:

<?php

namespace Tests\Unit;

use Tests\TestCase;

class HelpersTest extends TestCase
{
    /** @test */
    public function assert_string_is_a_greeting()
    {
        $stringA = 'Hello!';
        $stringB = 'Bye!';

        $this->assertTrue($stringA === 'Hello!');
        $this->assertFalse($stringB === 'Hello!');
    }
}

Now, let’s make a simple helper that accepts a string and tests whether this string is a greeting. For convenience sake, I’ll add this helper to a protected function in the same file, but this helper can be anywhere. A good place to put your custom assertions is in a separate package.

Let’s write two helpers:

use PHPUnit\Framework\Assert;

protected function assertGreeting(string $potentialGreeting): void
{
    Assert::assertStringContainsString('Hello', $potentialGreeting);
}

protected function assertNotGreeting(string $potentialGreeting): void
{
    Assert::assertStringNotContainsString('Hello', $potentialGreeting);
}

And use it like this:

/** @test */
public function assert_string_is_a_greeting()
{
    $stringA = 'Hello!';
    $stringB = 'Bye!';

    $this->assertGreeting($stringA);
    $this->assertNotGreeting($stringB);
}

This is way better, right? Much more concise and also much more clear what you’re exactly testing, in particular if you have team members.

Testing your testing helpers

But how do we know that our assertGreeting() and assertNotGreeting() helpers are asserting the right thing? In theory, they could be asserting totally different things and you’re tests would still pass. Let’s write a test for your own assertions!

In order to simplify the process, I’ll add the test in the same file again. If you’re putting your testing helpers in a package, you could add a test in your package tests folder.

First, let’s write a test that we can correctly assert a greeting, so the happy path. How do we do that?

When a validation fails, PHPUnit will throw an AssertionFailedError exception. We can catch that exception or specify that we expect such an exception.

In order to test a custom helpers, we can do the following:

  1. Input a few things that should pass, and do nothing. The helper should never fail, so the tests will be green.
  2. Input a few things that should not pass, and say that we expect an AssertionFailedError exception. The tests should also never fail now, because we say that we expect an exception.

So if we say that we don’t expect an AssertionFailedError for tests that should pass, and expect an AssertionFailedError for tests that should not pass, we can test our helper.

Using a dataprovider

Because we want to test as many inputs and ‘outputs’ as possible, I’m going to make use of a dataprovider.

If you haven’t used a PHPUnit dataprovider before, don’t worry, it’s very easy! It basically means that you have a function (which is the dataprovider) that returns an array. Each item in the array should be another array. PHPUnit will run the test for every item. That means that you don’t have to write ten very similar tests, but you can just use a single test plus a dataprovider for it.

First, we’ll create our dataprovider:

protected function greetingProvider(): array
{
    return [
        ['Hello', true,],
        ['Hello there!', true,],
        ['Bye', false,],
        ['Bye bye', false,],
        ['See you later', false,],
    ];
}

This dataprovider returns a multidimensional array. The first item in the ‘child-arrays’ is the greeting we want to test, and the second item is whether this is indeed a greeting.

Writing a test for our PHPUnit helper

Now, let’s write a test for our assertGreeting() helper:

/**
 * @test
 * @dataProvider greetingProvider
 */
public function can_correctly_assert_greetings(string $input, bool $isGreeting): void
{
    if ( ! $isGreeting ) {
        $this->expectException(AssertionFailedError::class);
    }

    $this->assertGreeting($input);
}

Let’s break this down. First we’re going to mark this as a @test and assign the greetingProvider() function as a dataprovider. Then, we’re saying: if the $input isn’t a greeting, expect an AssertionFailedError exception to be thrown.

Last, we’re going to run the assertion. Not that difficult, right?

Testing the assertNotGreeting() helper

We can also test our assertNotGreeting() helper in the same way as the first one. We’ll only expect an exception here if the $input is a greeting:

/**
 * @test
 * @dataProvider greetingProvider
 */
public function can_correctly_assert_not_greetings(string $input, bool $isGreeting): void
{
    if ( $isGreeting ) {
        $this->expectException(AssertionFailedError::class);
    }

    $this->assertNotGreeting($input);
}

Whole file

Below is a file with all the code I used for testing our custom PHPUnit assertion:

<?php

namespace Tests\Unit;

use PHPUnit\Framework\Assert;
use PHPUnit\Framework\AssertionFailedError;
use Tests\TestCase;

class HelpersTest extends TestCase
{
    /** @test */
    public function assert_string_is_a_greeting(): void
    {
        $stringA = 'Hello!';
        $stringB = 'Bye!';

        $this->assertGreeting($stringA);
        $this->assertNotGreeting($stringB);
    }

    protected function assertGreeting(string $potentialGreeting): void
    {
        Assert::assertStringContainsString('Hello', $potentialGreeting);
    }

    protected function assertNotGreeting(string $potentialGreeting): void
    {
        Assert::assertStringNotContainsString('Hello', $potentialGreeting);
    }

    /**
     * @test
     * @dataProvider greetingProvider
     */
    public function can_correctly_assert_greetings(string $input, bool $isGreeting): void
    {
        if ( ! $isGreeting ) {
            $this->expectException(AssertionFailedError::class);
        }

        $this->assertGreeting($input);
    }

    /**
     * @test
     * @dataProvider greetingProvider
     */
    public function can_correctly_assert_not_greetings(string $input, bool $isGreeting): void
    {
        if ( $isGreeting ) {
            $this->expectException(AssertionFailedError::class);
        }

        $this->assertNotGreeting($input);
    }

    protected function greetingProvider(): array
    {
        return [
            ['Hello', true,],
            ['Hello there!', true,],
            ['Bye', false,],
            ['Bye bye', false,],
            ['See you later', false,],
        ];
    }
}

Conclusion

Testing your own helpers in PHPUnit is not that difficult. It comes down to catching an AssertionFailedError exception. This is the perfect way to test your custom PHPUnit assertions!

Let me know if you’ve created a package with handy testing helpers and open-sourced it! I already created one such package for doing filesystem assertions. It it based on the Pest PHP testing framework, but it uses the same techniques as above. (Pest is a very nice wrapper around PHPUnit.)

Have fun, and, as always, if you have any questions or comments, please leave a comment below!⚡️

Stay up to date with all things Laravel, Tailwind, WordPress & PHP

Subscribe now to my e-mail newsletter and get my latest articles and project updates delivered directly to your inbox. Never miss an update.

Image Ralph J. Smit
Ralph is a designer gone developer. He happily lives in the Netherlands. His passion for good design drove him towards development, because he felt that no-code tools were too limiting. On this blog, Ralph writes the articles he would've wanted to have during his continual developer journey. → Follow on Twitter

Comments

Leave a reply

Your email address will not be published. Required fields are marked *

Close
Do you want more articles like this delivered straight to your inbox?

Subscribe now to my e-mail newsletter and get my latest articles and project updates delivered directly to your inbox. Never miss an update.