Unit Tested Controllers in Ruby On Rails
Fast tests good, slow tests bad.
Testing controllers in Rails using the default test setup is slow. In
fact testing a lot of Rails is slow, wether you use RSpec or TestUnit,
it makes no difference. The cause is the architecture of Rails, and the
default test setups which encourage you to write integration tests, often
without need. When it comes to your controller tests, even if your mockist
and using test doubles everywhere if your using
rspec-rails (or the test
unit equivalent) then Rails is being booted and the request generated by
passing through the stack down into your controller.
That’s slow! Slow tests lengthen the feedback cycle leading to interuptions in your train of thought and work, and this is what often leads people to question the usefulness of tests at all. There is an argument for not testing controllers at all, if you’re farming out all your logic to a service layer, or if you’re leaving it languishing in your models; but if your controllers are reacting to context changes, or handling errors properly, if there are any control path changes, you should test your controllers.
However in most of the test suites for Rails controllers that I’ve seen, the intent of the tests is to specify what a particular action does in those particular contexts. I’ve yet to come across, and I hope to never come across, a Rails controller action which reacts differently to GET/POST/PUT etc… it is better to route to different actions and test, if you must, those routes.
So rather than essentially writing integration tests for our controllers, why don’t we unit test them? A controller is a class in it’s own right after all, and an action is just a method. We can test that like we would test anything else.
Take an overly simplified example (a cliché of blog posts right?).
As you can see it’s not very exciting, notice though that we are doing the setup in the before block (in this case mocking out a service call with a set of params), and that in the subject we are just taking an initialized controller and calling the eventual method. There is nothing stopping you from turning this into an integration test (by not mocking the service call), but we have still not gotten the rails stack involved up to this point.
Ah but what about that magic require at the top I hear you say, what is that doing? Well…
It’s faking out a few things we might need to use, loading
ActionController, our base
ApplicationController and adding a helper
to initialize the controller in question. The fake request object passed
in allows you to configure the Rails controller staple’s of sessions and
params. Note you don’t have to stub out the redirect / render like I have,
it just makes it more isolated.
The net result of our tinkering? Sub second boottime, (all bundler), and this spec file runs in 0.002 seconds. So in future, prehaps you should think about your controller tests before you follow the rails way…
Thoughts? Questions? Queries? Trolling? Find me on Twitter