27
Testing Fat Laravel Controllers - Pt. 2
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.
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.
After authentication is handled, we must make sure that the endpoint doesn't allow empty requests or uploading file types other than images.
postJson
post
file
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.
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.
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.
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.
27