Unit testing your API Controllers
Updated: May 7, 2019
Just a couple of days back, Vivek and I came across a difference of opinion while planning the testing strategy for a project. The project in question was a Spring Boot based web application. As we all know, Spring provides a Model-View-Controller architecture to develop flexible and extensible web components. From initial analysis we found that, the project was quite structured and the multiple layers were easily discernible; and WITHOUT a single Unit Test. We started discussing the plethora of test components provided by Spring for different layers.
As is with the MVC architecture, the controllers are the API gateways and are supposed to forward the request to the service layer (which is solely responsible for business logic). I suggested Vivek that we start our tests with the controller layer. The classic approach, you know. Take an API, see what all functions it hits in the service layer and write tests for them.
“To start off,” I said, “let’s write UTs for the controller layer itself first and then move on to the service layer”.
“KP, that would be wrong,’’ he said, “We shouldn’t write tests for controller layer. The controllers do not contain any logical code. They just call functions in the lower layers (service layer).They get tested when you write your API regression tests. It is simply pointless and would lead to irrelevant tests”
My opinion differed as I strongly believe that unit testing the controllers can be significantly beneficial.
Ideally, you don’t write any business logic in your controller layer. It is supposed to just be a handler (routing mechanism) for the API call and should simply pass on the baton to some function in the service layer. So it does sound menial and over-engineering to write tests for this layer.
However, this notion is incorrect. There is quite a lot which a controller does.
It is responsible for mapping the incoming request to a function based on the API path, the HTTP method and the content type.
It then parses the incoming HTTP request and deserialises the request variables to apt Java objects.
The controller is your first line of defence. This is where your request is validated to catch faulty requests at an early stage.
The controller, post validation, would call the service layer to perform the required business logic and await a response.
On receiving the response from the service layer, the controller would deserialise the object into a JSON/HTML/XML object and create the apt HTTP response with required headers.
In all this, the controller is also the exception handler of the project. It is responsible for handling any exception and create the HTTP response accordingly.
Quite a lot of work, right? So it makes sense to test the controllers thoroughly. Ideally, with integration tests, you call the API and forget about all the HTTP/S magic happening in the controller. So basically you avoid all steps and just focus on step #4. This leaves some room for untested code leading to a possibility of undiscovered bug in the code. So testing the controllers is quite beneficial. Let’s look into some of these in detail.
Request validations — Often we write the validation of the request object in the controller itself. We verify the sanity of the request object and then pass it on to the service layer. It doesn’t make sense to actually write an over-the-network API call just to test the object validation. You can get away with it by writing apt UTs. This ensures that you don’t have to write negative tests as part of your regression and save some considerable amount of time there. Let’s take an elementary example. Say there is a username-password based login system which allows the user 3 attempts to enter correct credentials before blocking the user out of the system. To test this flow, would you make 3 API calls with incorrect mechanism to check the response of each API or have the controller unit tested to check the apt HTTP response being formed?
Exception handling — The service layer is expected to handle the business logic and return a response or an exception back to the controller. The controller is responsible to handle the exceptions thrown by the service layer. Now, it is absolutely tedious to write regression to test whether the exception is being handled aptly by the controller or not. We can again just test this part by Unit tests. No need to have negative regression tests. You simply mock the service layer call and make it deliberately throw an exception and voila! Your exception handling logic is tested without bringing up the web server at all.
Response validations — Just as it is with request, the response is also constructed by the controller. Once the service layer sends back a response, the controller would usually serialise into an object which complies with the contract between server and UI (JSON/HTML/XML). On top of that, there could be custom response headers being added. Once again, you wouldn’t want to hit the API to test all possible responses (positive/negative cases). Ideally, you would want to mock the service layer and check for all possible responses. Let’s take an elementary example. Say there is a username-password based login system which allows the user 3 attempts to enter correct credentials before blocking the user out of the system. To test this flow, would you make 3 API calls with incorrect mechanism to check the response of each API or have the controller unit tested to check the apt HTTP response being formed?
Assert fast and fail faster — API tests come with it’s own overhead. You need to bring up the entire web server, provide DB connectivity, ensure network availability. Of course they are time taking. With a growing project, your sanity time will keep increasing. In cases when you just want to check your APIs — whether the routes defined are working correctly or not, you wouldn’t want to rely on the regression. What if there was a way to bring your web server up with just the controllers. Mock all the other layers and just test whether the API is up and running or not. Well, there is a way. Spring provides the @WebMvcTest for this very reason. This allows you to bootstrap a single controller with all other layers mocked. How fun, right? Your API is up without any hassle of dependencies. Magic!
Although it is a good strategy to isolate one layer of tests, it could be migrainous if not implemented in the right manner. Lets see some common pitfalls as well :
Over-engineer — This could happen with any approach you try to follow. You might end up relying too much on your controller UTs. Writing too many tests for less cases is digging your own grave. Keep your tests restricted. The ideal way would be to see where the particular test fits in your test pyramid. Based on criticality, you might want to move it to integration test or simply write a UT. It is subjective and varies from project to project.
Costly overhead — If your tests for the controllers are not adding any additional test value or are duplicating test coverage, it would just be overhead and add development time for no benefit. Also, it adds to the maintenance weight. Your demarcation for a test case should be clear — whether it would be a unit test or an integration test. There is some scope of overlapping test cases, but quite rare.
Designing your test cases is an intricate process and the incorrect decision could add your execution time without any benefits. On one hand, you should definitely write UTs to test the ‘HTTP magic’ these controllers do. On the other hand though, you should be careful that you do not completely rely on them. Integration tests are equally valuable. It is just that you can shift some of the workload from integration tests to unit tests.
So what do you guys think? I am sure you would have some differing or additional thoughts..We will be following up on this soon with a code walkthrough on how to actually write tests for your controllers. Leave comments if you want us to cover something specific.
P.S. — Vivek and I did end up writing UTs for some of the controllers. ;)