Create a gist now

Instantly share code, notes, and snippets.

@adamwathan /0.md Secret
Last active Feb 15, 2018

What would you like to do?
Disabling Exception Handling in Laravel 5.4 Feature Tests

Disabling Exception Handling in Laravel 5.4 Feature Tests

A little over a year ago I shared a screencast on my blog showing a trick I use to get better feedback about errors when writing HTTP-level feature tests in Laravel.

The TL;DR is that when an exception happens in a feature test, you'll often get a crappy error about "expected a 200 response, got a 500, here's a shit ton of HTML in your terminal from the error page."

This happens because Laravel catches exceptions and turns them into error pages for you, which means the exceptions never hit PHPUnit itself.

I wrote a little helper to disable that high-level exception handling in Laravel in my feature tests, and these days I use it by default by calling it in the setUp method of my base TestCase class.

Occasionally you do want exception handling to happen, because you're trying to test some behavior that depends on it, like converting a ValidationException into a 422, or a ModelNotFoundException into a 404.

So any time I need exception handling to actually work, I stick ->withExceptionHandling() before the HTTP request to turn it back, effectively making it an opt-in behavior instead of opt-out like it would be by default.

I've provided my base TestCase file below, as well as a few example tests.

<?php
namespace Tests;
use App\Exceptions\Handler;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function setUp()
{
parent::setUp();
$this->disableExceptionHandling();
}
protected function disableExceptionHandling()
{
$this->oldExceptionHandler = $this->app->make(ExceptionHandler::class);
$this->app->instance(ExceptionHandler::class, new class extends Handler {
public function __construct() {}
public function report(\Exception $e) {}
public function render($request, \Exception $e) {
throw $e;
}
});
}
protected function withExceptionHandling()
{
$this->app->instance(ExceptionHandler::class, $this->oldExceptionHandler);
return $this;
}
}
<?php
namespace Tests\Feature;
use App\User;
use App\Product;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class CreateProductTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function adding_a_new_product()
{
$user = factory(User::class)->create();
$response = $this->actingAs($user)->post('/products', [
'name' => 'My First Product',
'price' => '39.50',
'description' => "Some information about my great product that you should buy.",
]);
$this->assertCount(1, $user->products);
tap($user->products->first(), function ($product) {
$this->assertEquals('My First Product', $product->name);
$this->assertEquals(3950, $product->price);
$this->assertEquals("Some information about my great product that you should buy.", $product->description);
});
}
/** @test */
public function requires_authentication()
{
$response = $this->withExceptionHandling()->post('/products', $this->validParams());
$response->assertRedirect(route('login'));
$this->assertEquals(0, Product::count());
}
/** @test */
public function name_is_required()
{
$user = factory(User::class)->create();
$response = $this->withExceptionHandling()->actingAs($user)->post('/products', $this->validParams([
'name' => '',
]));
$response->assertSessionHasErrors('name');
}
private function validParams($overrides = [])
{
return array_merge([
'name' => 'My First Product',
'price' => '39.50',
'description' => "Some information about my great product that you should buy.",
], $overrides);
}
}

Is there a reason you opted for an anonymous class versus an explicitly defined class?

I was reading that some stuck on 5.x are having to extract the class, so although I've borrowed the principle, I've separated the anon class into a regular class with Namespace loading.

As @Lewiscowles1986 mentioned with 5.x had to extract to base class, so code ended looking like:

class TestHandler extends Handler
{
    public function __construct()
    {
    }
    public function report(\Exception $e)
    {
    }
    public function render($request, \Exception $e)
    {
        throw $e;
    }
}

and the method:

    protected function disableExceptionHandling()
    {
        $this->oldExceptionHandler = $this->app->make(ExceptionHandler::class);
        $this->app->instance(ExceptionHandler::class, new TestHandler);
    }

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment