Testing middleware behavior with controller assertions

Main Thread 2 min read

Middleware is another area of Laravel applications which aren't often tested.

Instead, middleware is tested through another part of the application. For example, the auth middleware is tested through sending an HTTP request to a controller.

/** @test */
public function edit_displays_form()
{
    $user = factory(User::class)->create();

    $response = $this->actingAs($user)->get(route('user.edit'));

    $response->assertStatus(200);
    $response->assertViewIs('user.edit');
}

Laravel makes writing these types of integration tests super easy. As such, there's often not a need to test your middleware directly (a unit test).

Yet this strategy may yield one of two issues in either a lot of repetitive test code or a gap in test coverage.

Something like auth is a simple test as we've seen using actingAs. But what about custom middleware?

Let's consider a paywall which verifies the user has access to premium "add on" content. This is definitely something we would want to test and can do so again through an HTTP request.

/** @test */
public function index_restricts_access()
{
    $user = factory(User::class)->create();
    $product = factory(Product::class)->create([
        'sku' => 'Master'
    ]);
    $order = factory(Order::class)->create([
        'user_id' => $user->id,
        'product_id' => $product->id,
    ]);

    $response = $this->actingAs($user)->get(route('video.index'));

    $response->assertStatus(200);
    $response->assertViewIs('video.index');

    // ...
}

This work, but requires a lot of test setup. We could abstract this into a setup helper method or use a factory class. These may alleviate the duplication issue.

But the second issue still remains since most developers don't write thorough tests. They may write all this setup for one of the controller actions, but not for others. Thus creating a gap in test coverage.

We can address this issue by adopting a strategy which makes it easier to write these tests.

Our goal is to verify a controller action behaves like some other controller action. If we have tested one of the controller actions thoroughly, then we can simply assert they have the same linkage. In this case, the controller actions use the same middleware.

This can be done by verifying the underlying route for the controller action uses the expected middleware or set of middleware. I wrapped this code within an assertion named assertActionUsesMiddleware() and add it to the Laravel test assertions package.

Now instead of writing extra test setup code or leaving a gap in your tests, you can verify complex behavior with this simple assertion:

/** @test */
public function show_restricts_access()
{
    $this->assertActionUsesMiddleware(
        \App\Http\Controllers\VideoController::class,
        'show',
        'add-ons'
    );
}

Find this interesting? Let's continue the conversation on Twitter.