Joseph Lavin

Just a developer in the world

Writing Tests That Test Your Tests

8 years ago · 2 MIN READ
#Laravel  #PHP  #Testing 

If you go under the hood of Laravel's testing helpers you can see they are split "concerns". Each concern is a trait which is used in the core TestCase. In my own applications I have adopted this strategy and created my own concerns. For a good nights rest I need confidence that my custom assertions pass and fail when expected. But how do you test that a assertion has passed or failed? PHPUnit assertions are exceptions... you can catch them!

To assert an assertion passed

/**
 * @test
 */
function see_assertion_pass()
{
    // Arrange so the assertion will pass

    try {
        $this->assertTrue(true);
    } catch (Exception $e) {
        $this->fail("Assertion Failed When Expected To Pass:\n" . $e->getMessage());
    }
}

If the assertion fails the exception is caught and the test fails. Otherwise the assertion passed, so the test passes.

To assert an assertion failed

/**
 * @test
 */
function see_assertion_fail()
{
    // Arrange so the assertion will fail

    try {
        $this->assertTrue(false);
    } catch (Exception $e) {
        return;
    }

    $this->fail("Assertion Did Not Fail When Expected");
}

When the assertion fails the return statement will be executed. PHPUnit counts it as a passed assertion because an assertion was executed, but it did not catch an exception. If no exception is thrown (the assertion passes) then the fail statement is executed causing the test to fail.

When would this be used?

Recently I was writing some assertions for a custom notification system (not Laravel's Notification) . These assertions work with a collection of sent notifications. To avoid code duplication and to have more expressive tests I implemented a trait. Now I can use the trait on a test class and have clean readable tests.

class PageTest extends TestCase
{
    use InteractsWithNotifications;

    /** @test * */
    function it_sends_an_email_notification()
    {
        $this->post('/page/send-notification', ['email' => 'bob@bob.com']);

        $this->seeEmailNotificationSentTo('jon@weirwood.net');
    }
}

trait InteractsWithNotifications
{
    /**
     * @param $email
     * @return bool
     */
    private function seeEmailNotificationSentTo($email)
    {
        $found = $this->notificationHistory()->contains(function ($key, $notification) use ($email) {
            return $notification['to'] == $email;
        });

        $this->assertTrue($found, "Could Not Find Email Sent To [{$email}]");
    }

    // Other Assertions:
    // seeEmailNotification
    // seeInEmailNotification
    // dontSeeEmailNotification

    /**
     * @return \Illuminate\Support\Collection
     */
    private function notificationHistory()
    {
        // Logic to retrieve sent notifications here... this is a simple example
        return collect([
            ['to' => 'jon@weirwood.net', 'subject' => '...', 'body' => '...'],
            ['to' => 'ned@weirwood.net', 'subject' => '...', 'body' => '...']
        ]);
    }
}

Testing the InteractsWithNotifications concern:

class InteractsWithNotificationsTest extends TestCase
{
    use InteractsWithNotifications;

    /**
     * @test
     */
    function it_can_see_email_notification_sent()
    {
        // Arrange so assertion will pass... in this example case
        // nothing needs to be arranged.

        try {
            $this->seeEmailNotificationSentTo('jon@weirwood.net');
        } catch (Exception $e) {
            $this->fail("Assertion seeEmailNotificationSentTo Failed When Expected To Pass:\n" . $e->getMessage());
        }
    }

    /**
     * @test
     */
    function it_fails_when_it_can_not_see_email_notification_sent()
    {
        // Arrange so the assertion will fail... in this example case
        // nothing needs to be arranged.

        try {
            $this->seeEmailNotificationSentTo('sansa@weirwood.net');
        } catch (Exception $e) {
            return;
        }

        $this->fail("Assertion seeEmailNotificationSentTo Did Not Fail When Expected");
    }
}

Conclusion

The above is written using Laravel's TestCase, however the principle of using try and catch to tests the assertions can be applied to any PHPUnit test. The InteractsWithNotificationsTest could extend PHPUnit_Framework_TestCase instead of TestCase for better performance, however in production most likely you will need TestCase.

···

Joseph Lavin

An experienced back end and front end developer specializing in PHP & Vue.js.

comments powered by Disqus


Proudly powered by Canvas · Sign In