No More Seat Costs: Semaphore Plans Just Got Better!

    17 Feb 2020 · Software Engineering

    Testing Middleware in Laravel with PHPUnit

    9 min read
    Contents

    Introduction

    In this tutorial, we will show you how to test middleware in Laravel applications. If you are not familiar with the concept of middleware, it acts as a middle man between a request and a response. It’s a type of filtering mechanism. Using the AuthMiddleware in Laravel as an example, users who are not logged will always be redirected to the login page when trying to make requests which require authentication.

    Even though we write feature tests to verify a certain feature works as expected, it’s best to have unit tests for individual components in an application. This way, we can be sure that every component works as expected.

    Unit testing is not a new concept, but then how do we go about writing unit tests at the middleware level? We will use three examples for demonstration purposes – the first example will teach you how to test middleware that modifies request data. In the second example, we’ll test middleware that performs redirects based on a condition. In the final example, we’ll look into how we can test middleware that modifies a response.

    At the end of the tutorial, you will have learned:

    • How to create middleware,
    • How to create request objects manually,
    • How to modify request data, and
    • The concept of mocking when writing tests.

    Let’s get started.

    Testing Request Data

    Let’s say we have an application that lets users create posts. A post has a title and a body. While creating the title, some users might opt to capitalize the title while other users might want to write the title in lowercase. For uniformity, we want all the titles to be title cased before a post is saved to the database.

    In that case, we’d have some middleware responsible for converting all titles to title case. Below is a snippet showing what our middleware should look like:

    app/Http/Middleware/TitlecaseMiddleware.php

    [...]
    class TitlecaseMiddleware
    {
        public function handle($request, Closure $next)
        {
            if ($request->title) {
                $request->merge([
                    'title' => title_case($request->title)
                ]);
            }
    
            return $next($request);
        }
    }

    Notice we are calling merge() on the request object in order to modify the title. We’re doing this because it’s not possible to override request values in Laravel directly. Once we’ve changed the title, we then move to the next layer in our middleware stack as denoted by the line: return $next($request).

    So, how do we verify that this piece of middleware works as expected without building out the entire feature?

    [...]
    use App\Http\Middleware\TitlecaseMiddleware;
    use Illuminate\Http\Request;
    [...]
    class TitlecaseMiddlewareTest extends TestCase
    {
        /** @test */
        public function titles_are_set_to_titlecase()
        {
            $request = new Request;
    
            $request->merge([
                'title' => 'Title is in mixed CASE'
            ]);
    
            $middleware = new TitlecaseMiddleware;
    
            $middleware->handle($request, function ($req) {
                $this->assertEquals('Title is in Mixed Case', $req->title);
            });
        }
    }

    In the above test, we are creating a new request object, and then merging the title to the request object. Next, we pass in the request to the handle method of the middleware before asserting that the title was indeed amended. Let’s look at another example.

    Testing Middleware That Redirects Users

    Let’s generate the middleware and the test. The first example was just an overview of what middleware might look like and how to test it. We’re assuming that you already have a Laravel application that is up and running. If not, you can clone this repo as a starting point.

    Let’s work with a situation where we have middleware that checks whether a user is an admin, and if that user is not an admin and hits a certain route, they get redirected:

    php artisan make:middleware AdminMiddleware

    Then update our newly generated middleware to look this:

    app/Http/Middleware/AdminMiddleware.php

    [...]
    class AdminMiddleware
    {
        public function handle($request, Closure $next)
        {
            if (!\Auth::user()->is_admin) {
                return redirect('/');
            }
    
            return $next($request);
        }
    }

    All set, we can now create a unit test for this middleware:

    php artisan make:test AdminMiddlewareTest --unit

    Note that there will be a slight variation on how to go about testing this middleware:

    tests/Unit/AdminMiddlewareTest.php

    [...]
    use App\Http\Middleware\AdminMiddleware;
    use Illuminate\Http\Request;
    use App\User;
    [...]
    class AdminMiddlewareTest extends TestCase
    {
        /** @test */
        public function non_admins_are_redirected()
        {
            $user = factory(User::class)->make(['is_admin' => false]);
    
            $this->actingAs($user);
    
            $request = Request::create('/admin', 'GET');
    
            $middleware = new AdminMiddleware;
    
            $response = $middleware->handle($request, function () {});
    
            $this->assertEquals($response->getStatusCode(), 302);
        }
    
    
        /** @test */
        public function admins_are_not_redirected()
        {
            $user = factory(User::class)->make(['is_admin' => true]);
    
            $this->actingAs($user);
    
            $request = Request::create('/admin', 'GET');
    
            $middleware = new AdminMiddleware;
    
            $response = $middleware->handle($request, function () {});
    
            $this->assertEquals($response, null);
        }
    }

    To confirm that our tests are passing, let’s run PHPUnit:

    AdminMiddleware test

    Here are some explanations. First, we create new user objects, pass in values for the is_admin attribute in both cases and then create sessions for the users. After that, we create instances of Illuminate\Http\Request and pass them to the middleware’s handle() method along with an empty closure representing the response.

    For non-admin users, we get a 301 redirect after making a GET request to the /admin route. However, for admin users, we get a null response. This is because we are testing our middleware in isolation, meaning that there is no interaction between our middleware and other components in the stack.

    Testing Middleware That Modifies a Response

    For middleware that fetches the response and acts on it, things are a little more complex. We are going to use CORS for demonstration purposes. CORS is a security mechanism that allows a web page from one domain or Origin to access a resource with a different domain. From Wikipedia:

    Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served.

    php artisan make:middleware CorsMiddleware

    app/Http/Middleware/CorsMiddleware.php

    [...]
    class CorsMiddleware
    {
        public function handle($request, Closure $next)
        {
            $response = $next($request);
    
            $response->header('Access-Control-Allow-Origin', '*');
            $response->header('Access-Control-Allow-Methods', 'HEAD, GET, PUT, PATCH, POST');
    
            return $response;
        }
    }

    So how can we confirm the response has been modified and headers set?

    php artisan make:test CorsMiddlewareTest --unit

    tests/unit/CorsMiddlewareTest.php

    [...]
    use App\Http\Middleware\CorsMiddleware;
    use Illuminate\Http\Request;
    use Illuminate\Http\Response;
    [...]
    class CorsMiddlewareTest extends TestCase
    {
        /** @test */
        public function cors_headers_are_set()
        {
            $response = \Mockery::Mock(Response::class)
                ->shouldReceive('header')
                ->with('Access-Control-Allow-Origin', '*')
                ->shouldReceive('header')
                ->with('Access-Control-Allow-Methods', 'HEAD, GET, PUT, PATCH, POST')
                ->getMock();
    
    
            $request = Request::create('/', 'GET');
    
            $middleware = new CorsMiddleware;
    
            $middleware->handle($request, function () use ($response)) {
                return $response;
            });
        }
    }

    In the above test, we mock out the response object then check that the response has received two header calls, each being called with the right arguments. We then pass the request to the middleware, along with a closure that returns the response. The above test won’t make any assertions but had we failed to provide Mockery with the correct methods and arguments, our test would fail.

    To confirm our test is passing, let’s run PHPUnit:

    CorsMiddlewareTest

    Setting up Semaphore for Continous Integration.

    Being certain that all our tests are passing, let’s now set up continuous integration. This way, we can be sure that new additions or changes made to the codebase in the future won’t break things. If you’ve never used Semaphore before, sign up for a free account. Once you have logged in, follow these steps:

    1. Click the “Start a Project Button” button: Add new Project Screen
    2. Choose the account to create a project from. Semaphore supports GitHub and Bitbucket by default.
    3. Select the repository that holds the code you’d like to build: Select Repository Screen
    4. Select the branch you would like to build. The master branch is the default. Select branch

    After this step, Semaphore will start analyzing the project. Then get back to you with a new page to configure your project.

    1. Change the PHP version to 7: Project Configuration

    Leave the other settings as they are.

    1. Click on the “Build With These Settings” button at the bottom of the page. The build may take a few minutes but notice the first build fails? Failing Build

    Our tests failed because our application lacks an encryption key. This is usually the value set for the APP_KEY variable in the .env file. With Semaphore, we can directly copy the contents of our .env file into a new configuration file. Click on the Project Settings then Configuration Files on the left sidebar to create a new .env configuration file:

    New configuration file

    We should paste the contents of our .env file into the content section of the new configuration file like we did in the screenshot above. After that, save the new changes. Let’s run the build again.

    To get to the builds page, click on the application name towards the top left corner, then click the red button that says FAILED under the Branches section. You should be redirected to the page shown below:

    Rebuild revision

    Click the “Rebuild this revision” button. This time round, the build will pass. That’s it, we now have Semaphore set up. This way, our test suite will be run everytime we push to Git.

    Conclusion

    Although not a common practice, unit testing middleware ensures that we have a complete test suite. Also, it saves us time in that we don’t have to build out a whole feature, and then write feature tests just to be sure that some piece of middleware works as expected.

    Here is the link to the repo containing the examples.

    If you found the tutorial helpful, please hit the recommend button and don’t forget to share with friends. Feel free to leave any thoughts and suggestions in the comments section below. Cheers!

    2 thoughts on “Testing Middleware in Laravel with PHPUnit

    1. Good stuff, I didn’t realize it was so easy to generate requests from scratch! One bit of feedback — any time you’re writing unit tests, and then showing other people how to write unit tests, don’t forget to show the failure mode as well! It’s possible to write a test that passes but doesn’t actually prove that the code meets the specification.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Chris is a software developer at Andela. He has worked with both Rails and Laravel, and enjoys sharing tips through blogging. He also loves traveling. You can find him @vickris on Github, Twitter, and other places.