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