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:
with the steps implemented as:
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:
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.