Every so often I come across a question of controller or request specs and when to use them.
Before I can address this I need to get some terminology out of the way. In RSpec there are controller tests and request specs which are similar but subtly different. Let’s talk about what the difference is.
Controller specs vs. request specs
As of (apparently) July 2016, the Rails team and the RSpec core team recommend using request specs instead of controller specs. Here’s why, in their words:
For new Rails apps: we don’t recommend adding the rails-controller-testing gem to your application. The official recommendation of the Rails team and the RSpec core team is to write request specs instead. Request specs allow you to focus on a single controller action, but unlike controller tests involve the router, the middleware stack, and both rack requests and responses. This adds realism to the test that you are writing, and helps avoid many of the issues that are common in controller specs. In Rails 5, request specs are significantly faster than either request or controller specs were in rails 4, thanks to the work by Eileen Uchitelle1 of the Rails Committer Team.
If I had to extract a TL;DR from this I’d put it this way: “The RSpec team recommends that we use request specs instead of controller specs. The reason is that request specs provide a little bit more of a realistic test bed than controller specs”.
So henceforth in this post I won’t be mentioning controller specs, only request specs.
I only use request specs in three scenarios
My default policy is not to use request specs at all. I find that fine-grained model specs and coarse-grained feature specs give me a sufficient level of coverage. Plus the fact that I put very little code in my controllers means that controller-focused tests would add relatively little value.
There are three specific scenarios though where I find request specs helpful.
Scenario 1: legacy projects
If my controllers are skinny to the point of being skeletal then I don’t feel a strong need to test them directly (although I certainly still test the controller behavior indirectly with feature specs).
If on the other hand my controllers are morbidly obese, as they often are in Rails legacy projects, then I find request specs to be a very handy tool.
When I encounter a bloated controller method my first desire is just to scoop out the body of that method and move it to either a new or existing model object. Unfortunately that’s not always a safe option due to the code in that method being tightly coupled to the particulars of that controller. Adding to the risk is that I often don’t understand exactly what the controller method is supposed to do.
So in these scenarios I’ll write a characterization test in the form of a few request specs that cover that controller method’s behavior.
These request specs will bring me two benefits. First, the process of interrogating the code via tests will often help me gain a decent level of understanding of what the code is doing. Second, the tests themselves will provide a safety net that mitigates the risk of any change.
In this case my request specs hopefully have a limited lifespan. Hopefully my request specs will assist me in moving the controller code into models. Then I can reincarnate the request specs as model specs.
Scenario 2: APIs
I said earlier that I tend not to use request specs because the combination model specs and feature specs usually gives me the level of confidence I desire.
Since APIs don’t have GUIs that can be exercised, feature specs are out of the picture. So in these cases I instead use request specs.
Hopefully my API code is designed such that most of the heavy lifting is done by the models and tested by model specs. I don’t want to have to test a whole bunch of code path permutations via request specs. If there’s complexity to be tested, I want it to be tested via a model spec. To me a request spec on an API serves the same general purpose as a feature spec on a complete web application which is to answer the question: “Do all the layers of the application work together?”
When I’m coding a new “traditional” Rails app I actually disable request specs along with a handful of other spec types.
Scenario 3: it’s too awkward or costly to write a feature spec
There are some scenarios where it would be too awkward, impractical or slow to write a full feature spec for the controller behavior I want to test.
For example, let’s say I want the
delete action of a controller to allow deletion of the resource under certain conditions but not under certain other conditions. It might be too slow or cumbersome to write a full feature spec for this, so in this case I might just write a request spec.