Best Practices: Testing Rails controllers with RSpec
Originally posted on codecrate.com
Rails is primarily a web development framework and so it is natural that controllers are an integral aspect of your application. Controllers in Rails typically accept HTTP requests as their input and deliver back and HTTP response as output. This is a pretty important part of building web applications, and so unit testing your controllers should be one of the most fundamental parts of your testsuite. I'm a huge fan for RSpec for testing Rails applications, and I've found that although the rspec-rails gem is a great starting point, there is a general lack of best practices for how to properly unit test controllers.
Essential assertions
Let's start with the basics. What should you test? The output of Rails controllers is the HTTP response, and so it is essential that each controller action has tests that assert the core HTTP response properties. For example:
- What was the response status code?
- What was the response content type?
- Did the controller render the expected template?
- Did the controller render the expected Rails layout?
- Did the controller set any flash messages?
- Was any information inserted or deleted from the session?
- Did the controller redirect the user to a new URL?
Luckily, most of these assertions are a one-liner thanks to the excellent shoulda-matchers gem.
Structured contexts
Using RSpec contexts effectively are a critical aspect of writing great controller specs. Betterspecs.org does a good job providing the basics, and here are some additional tips that apply specifically to controller specs which will help make your tests much more expressive.
NOTE: The setup and assertion blocks (before/it) have been left blank as an exercise for the reader. The implementations should be very straightforward and were omitted to emphasize the structure/language used to scaffold the context blocks.
Input permutations
Create a new context for each set of meaningful inputs. Typically this is used for varying query parameters, but it also works well when you need to exercise code paths for HTTP session or header usage.
describe 'GET #index' do
context 'when params[:filter_by] == first_name' do
it 'filters results by first_name'
end
context 'when params[:filter_by] == last_name' do
it 'filters results by last_name'
end
end
Valid vs invalid params
Same premise as the previous tip, but worth calling out since it is such a common usecase.
describe 'POST #create' do
context 'with valid params' do
it 'redirects to show page'
end
context 'with invalid params' do
it 're-renders #new form'
end
end
Authenticated access
Use separate contexts for authenticated vs un-authenticated access.
describe 'GET #index ' do
context 'when user is logged in' do
it 'renders the listing page'
end
context 'when user is logged out' do
it 'redirects to login page'
end
end
Authorized access
Create a new context for each user role accessing the endpoint (ex: admin vs standard user).
describe 'GET #show' do
context 'as content owner' do
it 'renders the permalink template'
end
context 'as an admin' do
it 'renders the permalink template'
end
context 'as a guest user' do
it 'displays access forbidden message'
end
end
Avoid nested contexts
Do not nest contexts in order to share common setup. Just don't. Ever.
This deserves an entire blog post of its own and will be a topic of future discussion...
Controllers should render templates
Now, here's a gotcha with the RSpec-Rails integration. For some reason, the default RSpec-Rails configuration disables rendering of templates for controller specs. In my opinion, This is a very poor recommendation that leads to a false sense of security. I've seen this happen more than once where your controller tests will pass, your code coverage will be 100%, but like a HAML indentation issue will blow up in production. BOOM.
Rails controllers operate on HTTP requests and the response body is a critical part of it's job. To fix this, make sure to enable the render_views
setting in your rails_helper.rb file.
https://github.com/rspec/rspec-rails#controller-specs
RSpec.configure do |config|
config.render_views
end
I do encourage the use of view specs when a view has conditional codepaths, and I also recommend using integration/request specs when it is necessary to test Rack middleware, but it is unnecessary to write these extra specs for every single controller action and view. Why force developers to write separate integration tests that duplicate over 90% of your standard controller spec when I can simply render the views within controller specs and be done with it? I'm all for separation of concerns, but this is a simple matter of pragmatism vs dogma.
And, if there was a way to detect the code coverage of views, I actually would enjoy having controllers not perform this rendering call and could rely entirely on view specs. That way, any unrendered code paths in views could be caught by continuous integration processes. Until that is possible, I find it much safer to enable rendering by default and allow for specific controllers to opt-out of rendering if necessary.
Complete example
And here is a simple example for a controller action with structured contexts and the bare minimum expected assertions. Enjoy!
describe PostsController do
describe 'GET #index' do
context 'when user is logged in' do
with :user
before do
sign_in user
get :index
end
it { is_expected.to respond_with :ok }
it { is_expected.to respond_with_content_type :html }
it { is_expected.to render_with_layout :application }
it { is_expected.to render_template :index }
end
context 'when user is logged out' do
before do
get :index
end
it { is_expected.to redirect_to new_session_path }
it { is_expected.to set_the_flash(:warning).to('Please log in.') }
it { is_expected.to set_session(:return_to).to(posts_path) }
end
end
end
NOTE: the RSpec with
helper method comes from the handy factory_girl_rspec gem and the respond_with_content_type
matcher from the shoulda-kept-respond-with-content-type gem.
Written by Ryan Sonnek
Related protips
2 Responses
nice very helpful!!!
I agree with most of your post, however performing these tests by each of the actions in each of the controllers of an application:
it { is_expected.to respond_with_content_type :html }
it { is_expected.to render_with_layout :application }
is too much in my opinion.