Create a gist now

Instantly share code, notes, and snippets.

@adamwathan /0.md Secret
Last active Jul 18, 2018

Embed
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);
}
}
@Lewiscowles1986

This comment has been minimized.

Show comment
Hide comment
@Lewiscowles1986

Lewiscowles1986 May 20, 2017

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.

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.

@connor11528

This comment has been minimized.

Show comment
Hide comment
@connor11528

connor11528 May 29, 2017

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!

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!

@AhmedHelalAhmed

This comment has been minimized.

Show comment
Hide comment

Thank you

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