Tips on testing PHP exceptions

Intro

Let's have a shamefully simple class like this:

<?PHP

namespace App;

class Example
{

    public function handle()
    {
        throw new \Exception('Not Found', 404);
    }
}

Back in the days, there were three options for how to test the exception. From PHPunit 9.0 onwards, only two remain. expectedException annotation was deprecated and removed.

Now we can concentrate on existing solutions.

Try Catch

First one is a try-catch pair. I've seen Adam Wathan used it and advocate for it in his TDD course. Some devs consider this to be a big old fashioned. However, it has one advantage. Inside catch block, you can run additional assertion related to exception or even some other parts of the code, e.g. user wasn't updated.

/** @test*/
function exception_is_thrown()
{
    try {
        $example = new Example();
        $example->handle();
    } catch (\Exception $e) {
        $this->assertSame('Not Found', $e->getMessage());
        $this->assertSame(404, $e->getCode());

        // additional assertion e.g. user wasn't updated
        return;
    }

    $this->fail('Exception was not thrown.');
}

Expectations

This approach is the PHPunit's recommended one. You can use four different methods expectException, expectExceptionMessage, expectExceptionMessageMatches. Be aware that apart from those 4, you can't use any other assertions. If you would add $this->assertTrue(false); it would be ignored at the end of the test.

/** @test */
function exception_is_thrown()
{
    $this->expectException(\Exception::class);
    $this->expectExceptionCode(404);
    $this->expectExceptionMessage('Not Found');
    $this->expectExceptionMessageMatches('/Found/');

    $example = new Example();
    $example->handle();
}

Conclusion

They are both excellent strategies to test PHP exceptions. Personally, I use the second one by default. I would use the first one only if additional assertions are needed.