Skip to content

Instantly share code, notes, and snippets.

@adamwathan

adamwathan/0.md Secret

Last active December 1, 2023 08:20
Show Gist options
  • Save adamwathan/125847c7e3f16b88fa33a9f8b42333da to your computer and use it in GitHub Desktop.
Save adamwathan/125847c7e3f16b88fa33a9f8b42333da to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link

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
Copy link

Thank you

@EricBusch
Copy link

It looks like as of Laravel 5.5 you can add $this->withoutExceptionHandling(); to the top of your test methods to disable exception handling.

laravel/framework@a171f44

@magarrent
Copy link

I'm using ÑLaravel 5.8, I have to put the "void":

protected function setUp(): void {
    	parent::setUp();
    	$this->withoutExceptionHandling();
    }

@AtlantisPleb
Copy link

AtlantisPleb commented Apr 17, 2020

In Laravel 7.5 I got "Declaration of class@anonymous::render($request, Exception $e) must be compatible with App\Exceptions\Handler::render($request, Throwable $exception)"

Fixed by changing \Exception to \Throwable on lines 25 and 26

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