An Approach to Testing Middleware

October 2017
This post is over a year old. Some of this information may be out of date.

I’ve always tested middleware in one of two ways, a unit test with mocks and asserting the callback in the handle method or integration tests on application routes.

Unit Testing

The problem with unit testing is that you don’t have any visibility into if the middleware is actually active and you also don’t know how it will function as the request is passed through other active middleware.

Integration Testing

Using integration tests on application routes resolves some of the issues with unit testing middleware but it makes it a little bit harder to make assertions on how the middleware is actually functioning. I also found myself wanting to constantly verify that the middleware was active and working on each of my application routes.

Testing the Middleware

I wanted to find a simple approach that verified the following:

  • Middleware is active, and configured correctly (global, group, single use)
  • Middleware is functioning correctly

Lets take a look at some examples

use App\Account;
use Tests\TestCase;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpFoundation\Request;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ForceJsonMiddlewareTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function forces_json_response()
    {
        $this->actingAs(factory(Account::class)->create());

        // Needs to be part of the global middleware because of how exception
        // rendering works
        Route::get('force-middleware', function (Request $request) {
            return response()->json([
                'accept_header' => $request->header('Accept'),
            ]);
        });

        $response = $this->get('force-middleware', [
            'Accept' => 'text/xml'
        ])->json();

        $this->assertEquals($response['accept_header'], 'application/json');
    }
}
use App\Account;
use App\HttpLog;
use Tests\TestCase;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Testing\RefreshDatabase;

class HttpLogMiddlewareTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function logs_http_requests()
    {
        $account = factory(Account::class)->create();
        $this->actingAs($account);

        Route::middleware('api')->group(function () {
            Route::post('httplog-test', function () {
                return response()->json([
                    'hello' => 'world',
                ]);
            });
        });

        $this->post('httplog-test', ['foo' => 'bar']);

        tap(collect(HttpLog::first()), function ($log) use ($account) {
            $log->forget(['id', 'created_at', 'updated_at', 'agent']);

            $this->assertEquals($log->toArray(), [
                'account_id' => $account->id,
                'url' => 'httplog-test',
                'method' => 'POST',
                'request' => ['foo' => 'bar'],
                'response' => ['hello' => 'world'],
            ]);
        });
    }
}

So what I’ve done here is define custom testing routes and applied the middleware as I would use it in the application routes, in this case global middleware and as middleware for the api group. This allows me to assert that the middleware is configured and functioning correctly.

The only potential drawback to this approach is that it still relies on the developer to ensure the application routes are assigned middleware correctly. This could still be mitigated by testing what middleware is active on each route. This isn’t something I typically worry about though.

class PostIndexRouteTest extends TestCase
{
    /** @test */
    public function route_has_assigned_middleware()
    {
        $this->assertEquals(
            Route::getRoutes()->getByName('posts.index')->gatherMiddleware(),
            ['web', 'can:view-posts']
        );
    }
}