How I Test Controllers, 2009 Remix
January 15, 2009
A while back I wrote about how I test controllers. I'd like to share my current approach to controller specs, which is significantly different. I've adapted my practice to align with something I've found to be generally true:
Controller specs don't matter, because controllers don't matter
The tail end of that statement is the important point. It's not that controllers don't matter, because of course we need them. What I mean though is that you should have little to no interesting logic in the controller. Any logic should be
(a) in the model (skinny controller, fat model)
or
(b) in a (mini-)framework
When all the logic is in the model or in framework code, the controllers become uninteresting bits of glue. Which then makes them incredibly easy to test - you just poke it and make sure nothing blows up.
Testing gluey bits
If you're going to poke something gluey like a controller, you get max value by hooking up all the attachable parts. This means in a Rails app, you're going to want to go through routing, filters, the real model including associations, the db, and all the way back up to the rendered view. Welcome to Integration Testing 101.
Using Cucumber to test my controllers
If I'm using cucumber on a project, I use it to test my controllers. I write acceptance tests and then implement them using webrat to fill out forms, follow links, etc. Cucumber's really nice because it covers every piece I mentioned above. It tests every part of the app stack except for Javascript, really.
I would have a Cucumber feature that includes:
Scenario: Create a new blog post
Given I am at the new posts page
When I create a post named 'My blog post'
Then I should see 'My blog post' in the list of posts
with the steps implemented as:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Given /^I am at the new post page$/ do visit new_post_url end When /^I create a post titled '(\d*)'$/ do |title| fills_in :title, :with => title fills_in :body, :with => 'blah blog body' click_button 'Create' end Then /^I should see '(.*)' in the list of posts$/ do |title| response_body.should include(title) end |
That one test lets me know that
- GET /posts/new renders without errors
- POST /posts creates a new post and redirects to the index
- GET /posts renders without errors
Which is enough to make me happy about the coverage, given that my controllers are brain-dead simple. It's also less work...I don't have to test that the post count increased, or the right template was rendered, or that it redirected to the right place.
or as DHH might say, "Look at all the things I'm *not* testing!"
One thing I must point out is that I don't write controller specs if my controllers are tested through cucumber! I might wrote a couple if there are edge cases or important alternate paths, but I keep those to a minimum. I absolutely do not write the typical controller specs, testing the same stuff over again - that's a waste of time, both when you first write it and when you have to maintain the test in two places later on.
sans-Cucumber alternative: "ping" specs
Maybe you haven't been bitten by the Cucumber bug yet. Sometimes I do projects without Cucumber, because I don't have an end customer that I need to communicate with. I use "ping" specs to test my controllers, so-named because I just ping the controller and see if it responds (mostly).
Controller specs are more verbose than cucumber features because we're working at a slightly lower level thus we must specify different things.
Here's a typical controller spec:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
describe PostsController do integrate_views # UBER IMPORTANT!! describe "GET /posts" do it "should be ok" do create_post get :index response.should be_success end end describe "GET /posts/1" do it "should be ok" do p = create_post get :post, :id => p.to_param response.should be_success end end describe "GET /posts/new" do it "should be ok" do get :new response.should be_success end end describe "POST /posts" do def do_post post :create, :post => {:title => 'blog title', :body => 'body'} end it "should create a new post" do lambda { do_post }.should change(Post, :count).by(1) end it "should redirect to the posts index" do do_post response.should redirect_to(posts_url) end end describe "PUT /posts/1" do before(:each) do @post = create_post end def do_put put :update, :id => @post.to_param, :post => {:title => 'new title'} end it "should update the post" do do_put @post.reload.title.should == 'new title' end it "should render the post" do do_put response.should render_template('show') end end describe "DELETE /posts/1" do before(:each) do @post = create_post end def do_delete delete :destroy, :id => @post.id end it "should destroy the post" do do_delete lambda {@post.reload}. should raise_error(ActiveRecord::RecordNotFound) end it "should redirect to the index" do do_delete response.should redirect_to(posts_url) end end end |
I do test a couple other things, such as failure paths (with create/update, and sometimes destroy) and flash. The example above shows the essence of my strategy. I want to get a lot of mileage from my test by hitting it at a high level and using the whole stack. The only piece missing from here is the routing.
Simple code is awesome, frameworks are awesome
For this to be effective, you have to have wicked tight controller code and thoroughly tested models. Tight controllers mean that you can be confident with minimum testing because there's not much stuff in the controller that can break. The easiest way to get to that point is by using a framework such as resource_controller, or rolling your own simple one. Then be sure your models are well tested, because that's where most of the logic and potential for error sits.
This can also be a useful strategy when dealing with absurdly messy controller code. The goal again is to cast a big net with your test, but now the reason is so that you'll be alerted quickly if you break anything. It might be tough to decipher the problem because you don't have good isolation, but at least you have some safety net. The trick though is to know what code isn't being tested, or isn't being tested enough, so that you can make careful changes. You can find techniques for analyzing code, identifying and creating seams, and working with shit code in general in the awesome book Working Effectively with Legacy Code. I'm also going to be talking about this, especially as it applies to Rails projects, at Scotland on Rails in March.
blog comments powered by Disqus
