Testing Laravel Exception Rendering

Mike Smith
2 min readNov 18, 2019

You can throw Exceptions, but can you catch them nicely for your users?

Photo by Max Chen on Unsplash

Having logic in your controllers to handle unexpected external issues can make your controllers ugly and difficult to manage. Accordingly, Laravel provides the exception handler by default at App\Exceptions\Handler. This does all sorts of handy wrapping, and is great for reporting (bugsnag, honeybadger etc use this). But a handy feature I recently discovered is that if you create custom exceptions with a render method, it will use that to produce a response for your users.

This means that you can keep the end result of your custom exceptions inside their definitions, rather than having a large amount of handling in the core Handler. (I started off looking to do this in middleware, but the structure of Laravel does not to lend itself so well to this, so this is the next best thing).

class ExternalObjectNotSetupException extends \RuntimeException
{
/** @var ExternalObject */
public $externalObject;

public function __construct(ExternalObject $externalObject, $code = 0, Throwable $previous = null)
{
$this->externalObject = $externalObject;

parent::__construct("External dependency {$externalObject} is not setup correctly.", $code, $previous);
}

public function render(Request $request)
{
return redirect(route('external-object.view', $this->externalObject))->with([
'error' => 'Your external object is not configured correctly.'
]);
}
}

Having created the above custom exception, my basic plan is that all sorts of issues that might arise with this external dependency can be wrapped in this parent Exception. So for my various service components I can test that this exception is thrown, but I want to ensure that whenever this exception is thrown, the user will be taken to the detail page of the object with the issue. Rather than testing each specific circumstance, I have a single feature test:

class UserVcsRepoExceptionTest extends TestCase
{
use DatabaseTransactions;

/** @test */
public function exception_triggers_redirect_to_detail_view()
{
$externalObject = factory(ExternalObject::class)->create();
$e = new ExternalObjectNotSetupException($externalObject);

$this->mock(Router::class, function ($mock) use ($e) {
$mock->makePartial();
$mock->shouldReceive('dispatch')
->andThrow($e);
});

$response = $this->actingAs($externalObject->user)
->get('/');

$response->assertRedirect(route('external-object.view', $externalObject));
}
}

By throwing from the dispatch method, I know I’m throwing the exception as though from any request, and I can trust that wherever I throw this exception, my application is going to take the user somewhere more informative.

--

--