Testing HTTP Middleware in Laravel
Learn how to create and test Laravel middleware to enhance API functionality while ensuring stability. Includes examples and tips for best practices
HTTP middlewares allow APIs to have amazing and powerful functionality added to requests and responses without needing to mess with each controller. Laravel has its own middleware implementation, with core middlewares handling CORS, caching, authentication, etc.
Custom middlewares can be defined to standardize behavior across an API and to change the default way Laravel works.
For example, the Protect Earth API likes to default to Accept: application/json
if no header has been sent, but should not override that if a different accept header has been sent.
# app/Http/Middleware/DefaultToAcceptJson.php
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class DefaultToAcceptJson
{
public function handle(Request $request, \Closure $next): Response
{
// If the request does not have an Accept header, set it to application/json
if ($request->header('Accept') === null) {
$request->headers->set('Accept', 'application/json');
}
return $next($request);
}
}
That'll probably work, but adding functionality like this without an accompanying test is reckless and will probably lead to an angry email, so let's pop a test on.
Testing Laravel Middleware using Pest
The idea is pretty much the same whether using Pest or PHPUnit: create an empty request, spin up an instance of the middleware, then add some assertions.
<?php
declare(strict_types=1);
use Illuminate\Http\Request;
describe(App\Http\Middleware\DefaultToAcceptJson::class, function () {
it('will set the Accept header to application/json', function () {
$request = new Request();
$middleware = new App\Http\Middleware\DefaultToAcceptJson();
$middleware->handle($request, function ($request) {
expect($request->headers->get('Accept'))->toBe('application/json');
return response()->json();
});
});
it('will not overwrite the Accept header if it is already set', function () {
$request = new Request();
$request->headers->set('Accept', 'text/html');
$middleware = new App\Http\Middleware\DefaultToAcceptJson();
$middleware->handle($request, function ($request) {
expect($request->headers->get('Accept'))->toBe('text/html');
return response()->json();
});
});
});
The $middleware->handle(
part is confusing at first, but with a bit of thinking about it we can make it make sense.
Middlewares can modify the request, and/or modify the response.
Testing request middlewares
When modifying the request, it's usually adding or changing some headers, then the request moves along the stack to the next thing, which could be another middleware or invoke the controller.
The line $middleware->handle($request, function($request)
middleware is "handling" the $request
, doing whatever it's done, then moves onto the next request. At this point, the middleware should have done it's thing, so expectations can be set inside here.
it('will not overwrite the Accept header if it is already set', function () {
$request = new Request();
$request->headers->set('Accept', 'text/html');
$middleware = new App\Http\Middleware\DefaultToAcceptJson();
$middleware->handle($request, function ($request) {
expect($request->headers->get('Accept'))->toBe('text/html');
return response()->json();
});
});
The response()->json()
is just there to keep PHP signatures valid.
Testing response middlewares
Instead of hopping into the request chain to see if a request was modified, testing a response middleware means we need to grab the last result coming out of the whole request chain.
it('will shove out a Content-Type header', function () {
$request = new Request();
$middleware = new App\Http\Middleware\SomeResponseMiddleware();
$response = $middleware->handle($request, fn(): string => 'response');
expect($request->headers->get('Content-Type'))
->toBe('application/json');
});
There it is, middleware can add amazing functionality to a website but should not be left to change.
Slap a test on it today and keep APIs working properly.