Testingmania #3: Testing Fat Laravel Controllers - Pt. 2

Geni Jaho

5 min read

Before we start with the second part of testing our fat controller, let's clarify what we should have from the start, that these tests are meant to be carried out like this in a certain dev environment. Usually, you test and refactor at the same time, meaning you write a test case for a piece of code and then take that code and extract it into its own method or class.

However, in our previous post, we wrote a gigantic test method, that tested almost all of the pieces of functionality. And that is not optimal. You should in general make small test cases, so when one breaks, you'll know what's wrong. However, if you need something fast, and you have a lot of world-building to do, you can just go the other way. Also, it seemed pretty risky refactoring at this stage, since there was no test coverage at all.

Anyway, going further we're testing if unauthenticated users can upload photos, whether they're allowed to upload files other than images, whether the images get uploaded to an S3 bucket, and more.

Checking for unauthenticated users

Testing that we have authentication checks is pretty simple. We don't even need to upload a real file to the server because authentication checks must be done before everything else. If that's not the case we should see validation errors. Anyway, we just make a post request with some dummy data, and, sure enough, we should be redirected to the login page.

Testing for requests validation

After authentication is handled, we must make sure that the endpoint doesn't allow empty requests or uploading file types other than images.

So we log in as a user and make a request for the first case of empty requests. You'll notice we're using a postJson method because for this particular case we need to test for the correct validation errors. That might have been difficult to impossible using post. The status 422 check makes sure that the server will notify the user that something went wrong and more specifically with the file input.

The other case is when we upload files other than images, for example, a file ending in .pdf. We assert the same server errors in this case.

Testing for expected errors

Looking at our PhotosController we notice that it throws an error every time it encounters an image that doesn't contain location data. It's easy to test this case because, by default, Laravel's fake files have no location data. We just log in, create a fake image, try to upload it, and test that the server returns with a 500 error code. And that's it. We could also check for the exact error message, but since the current implementation of the error pages doesn't show the error, we only check for the status code.

Testing for production environments

There is a catch with this one. On our controller, we have a condition that makes the code behave differently depending on the environment. If we're in a production environment, the uploaded image will be stored on an S3 disk, otherwise, it will be stored on the public/local-uploads directory. We've already tested for non-production environments, let's test it the other way.

Aside from the usual world-building, we need to add this call to app()->detectEnvironment() from which we swap the environment to production during this test's lifetime.

Then we make the request and notice something strange. A CSRF token mismatch error appears right out of the blue. Here's what's happening: in testing environments, the app ignores the CSRF checks and allows you to make POST requests without including CSRF tokens. However, since we swapped the environment to production, the app now requires us to include those tokens and throws the error.

The simplest solution that comes to mind is using a trait called WithoutMiddleware, which basically tells the app to ignore all middleware. After we use this trait the test works fine, the image gets uploaded to the S3 disk and has the right properties. However, the authentication test is now failing, because, duh, we disabled all middleware for the test class, including the auth middleware. Too bad. At this point, we just extract this last test for production environments into its own class, called UploadPhotoToProductionTest.

This way, if we need to add more tests for production environments, we'll have a dedicated class for them, without breaking other tests. This, of course, until this change in behavior depending on the environment is fixed, as it should.

Oh and one more thing. Ideally, we'd write one more test, one that doesn't fake the Storage, but instead goes all the way and uploads the image to a real S3 bucket. This would make sure that the upload really works in production. We'd delete the uploaded image at the end of the test, of course.

The code used for illustration is taken from the OpenLitterMap project. They're doing a great job creating the world's most advanced open database on litter, brands & plastic pollution. The project is open-sourced and would love your contributions, both as users and developers.