tjbp
tjbp

Reputation: 3517

How can I override Laravel's exception handler in PHPUnit tests?

I am developing a Laravel package that includes controllers, which I wish to unit test. The problem is if an exception is thrown inside one of these controllers, Laravel's exception handler captures it and outputs a 500 HTTP response. PHPUnit is none the wiser, and tests simply fail to meet the 200 OK assertion. The lack of a stack trace in PHPUnit's output both locally and on services such as Travis CI hinders workflow enormously.

I'm aware that I could rethrow the exception from somewhere such as \App\Exceptions\Handler, but since this is a package, I can't modify those application files (laravel/laravel is simply a dependency for testing, in order to rope in the necessary components for testing controllers).

I would have thought the set_exception_handler() call below in TestCase would work, but weirdly it has no impact whatsoever:

public function createApplication()
{
    $app = require __DIR__.'/../bootstrap/app.php';

    $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

    set_exception_handler(function ($e) {
        throw $e;
    });

    return $app;
}

Can anyone tell me why the above doesn't work?

Upvotes: 4

Views: 2255

Answers (2)

patricus
patricus

Reputation: 62278

Your attempt to reset the exception handler where you did won't work (as you've already found out). When you use the call() method to make a request, Laravel makes a new instance of the Http Kernel, and when it handles the new request, it bootstraps itself and runs the HandleExceptions bootstrapper, which of course resets the exception handler again.

You may not have access to modify the \App\Exceptions\Handler class, but you don't need to. All you need to do is create your own exception handler, and then update the binding in the IoC container to use your exception handler, instead of the one provided by the Laravel application.

So, first, create your new exception handler:

<?php

class MyExceptionHandler extends \App\Exceptions\Handler
{
    public function render($request, \Exception $e)
    {
        // You may want to keep all these cases for special exceptions.
        // If not, just get rid of these lines.
        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        // Not a special exception. Just re-throw it to get meaningful output.
        throw $e;
    }
}

Now, in your createApplication() method, update the exception handler binding:

public function createApplication()
{
    $app = require __DIR__.'/../bootstrap/app.php';

    // Tell the tests to use your exception handler.
    $app->singleton(\Illuminate\Contracts\Debug\ExceptionHandler::class, MyExceptionHandler::class);

    $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

    return $app;
}

Upvotes: 1

Alex Blex
Alex Blex

Reputation: 37048

Unittests should test individual units, not the whole application.

There are great chances you are doing functional or integration testing.

When you test controllers, and expect 200 response, everything you need to know is response code. 500 response fails the test, which indicates malfunction or misconfiguration. CI tool should only show number of passed/failed tests.

Testing is not debugging and should not provide any special backtraces. If you feel you don't have enough information about nature of the error, you should review your error handling and logging. Otherwise you will face the same problem with lack of information when errors happen in production.

Upvotes: 2

Related Questions