Test behaviour with Request specs rather than testing implementation with Controller specs

Anson Kelly
Carwow Product, Design & Engineering
5 min readJul 6, 2017

--

Test behaviour of the entire stack, not just part of it

At carwow we have been upgrading our Rails apps from 4.2.x versions to the shiny new 5.0/5.1 versions.

One of the changes moving from Rails 4 to Rails 5 is the deprecation of controller tests in favour of higher level request specs.

Our primary Rails applications have been around for a few years now (the oldest app still in production started out as a Rails 3.2.1 application ~5 years ago) so there are 1000’s of controller tests that have been written over their lifetime.

When testing web applications I find that approaching the tests with the mindset of how the application will be used, rather than what the application is made up of produces a more robust and useful test suite.

As such this blog post doesn’t really introduce anything new that has not been said elsewhere. It’s purpose is to centralise the reasons to convert existing legacy controller specs into request specs that we can point new developers to. It also might help others.

Examples are using Rails 5.1 + RSpec 3.5

Controller specs

These have been around since the early days of Rails and have historically been the way to test that web requests “do the right thing(s)”.

Controller tests while looking like they test the request behaviour actually do not — they test the implementation of the controller object.

A (contrived) example:

describe SessionsController do
describe 'GET #new' do
it "renders the login page" do
get :new
expect(response).to render_template('sessions/new')
end

it "assigns the session" do
get :new
expect(assigns(:session)).to be_a_new(Session)
end
end

describe "POST #create" do
it "redirects to the dashboard" do
post :create, session: FactoryGirl.attributes_for(:session)
expect(response).to redirect_to dashboard_url
end
end

# other examples ...
end

Here we have an examples of how controller tests are invasive and brittle.

Both assert_template and assigns assert the implementation, not the behaviour. Do our users care what the template is named, or what the name of the instance variable is? Nope.

In both cases what actually matters is the request processing behaviour and the response. Everything else is an implementation detail. We can rename the template or change the instance variable name and as long as the tests still pass then the behaviour remains the same and our users are happy.

Another thing to note is the action of each test eg get :new This is calling the new method on the controller. However that is not how our login behaviour works in the real world — a request first passes through the middleware stack and is then routed to the controller. So what we are doing here is skipping out a whole lot of behaviour that may change the request. This is another example of how controller specs test implementation rather than behaviour. Does it matter what the controller action is named? Nope not really (I’m purposely avoiding RESTful considerations here)

tldr: We should be testing behaviour not implementation.

Request specs

Are the way to test the behaviour of a request in a way that is much closer to the way the code will execute in a production environment.

Lets rewrite the above example as a request spec using capybara to mimic the behaviour of a user:

describe 'Authentication' do
it 'allows users to log in' do
user = FactoryGirl.create(:user, password: 'password')
get new_sessions_url fill_in 'email', with: user.email
fill_in 'password', with: 'password'
click_button 'Login'

expect(current_url).to eq(dashboard_url)
expect(page).to have_selector('.message', text: 'Welcome')
end
# other examples ...
end

The first thing to notice is we now have 1 test rather than several. This is a more realistic example as we are now thinking in terms of behaviour, not implementation. Loading and submitting the login form is 1 action from a users perspective.

get sessions_url is using our routes to perform the action, not just calling a method directly. This is now explicitly testing that the route is set up correctly and that the middleware is not changing the request in unexpected ways.

We are testing the response to the new action by filling in the login form and submitting it in just the same way a user would. This is testing not only that the form appears to the user as expected, but that it also captures and submits to the expected location.

Then we assert that we end up on the dashboard (which is a protected route) with the correct message showing.

Other benefits

While having tests that mimic user interaction are a win there is another benefit — our implementation just got a lot easier to refactor.

With our previous controller spec changing the instance variables or template name would mean changing one or more tests, which then could mean altering the implementation to allow the tests to be changed and makes the whole process harder.

With the request spec version those changes can be made transparently- as long as the behaviour remains the same then we are free to refactor to our hearts content.

Speed

One of the arguments against using higher level (behavioural) tests is that they are slower. Lets see by running the above examples:

The controller specs:

bundle exec rspec spec/controllers/sessions_controller_spec.rb
...
Finished in 0.17622 seconds (files took 1.74 seconds to load)
3 examples, 0 failures

And the request specs:

bundle exec rspec spec/requests/sessions_spec.rb
.
Finished in 0.22617 seconds (files took 1.72 seconds to load)
1 example, 0 failures

As shown the request spec is slightly slower, but tests far more of our applications behaviour. Like most choices in the development world this is a tradeoff — test robustness against raw speed.

Obviously with these more than slightly contrived examples its not an absolute that request specs will be faster, but its safe to say they are not much slower.

Wrapping it up

Feature specs are the way to test your application’s behaviour closer to the way users will interact with it. The implementation details are just that — and should not have to be explicitly tested.

This helps us develop a test suite that describes our application’s behaviour, prevents regressions, and allows freedom to refactor implementation details without have brittle tests.

How many fragile and brittle controller tests still lurk in your Rails codebase? Is is time to upgrade them to request specs?

--

--