I have to hand it to you: you're a great Rails developer! I just read through the code you've been writing for that new project and you're doing it right. You've got fast, isolated tests with RSpec, integration tests in well-written Cucumber scenarios, and have you lost weight or are your controllers skinnier? Just one nit-pick, though—where are your controller specs?
If you follow best practices, I understand that it can seem unnecessary to test your controllers: they're so small, they are all the same, and it's pretty hard to get it wrong. Indeed, it would probably take longer to write the tests than it would to write the controller itself and in any event you already have integration tests that are covering most of the controller code.
Let me tell you why I still think you should write controller tests:
- They are very easy to write
- They are mostly the same
- Because they are mostly the same, we can refactor and make them even easier to write
We all refactor our code, but it's just as important to refactor our tests. The goal of refactoring is to facilitate reuse and increase clarity. Refactoring let's us develop new abstractions upon which we can build complex logic. Our controllers are so lean and mean because they are benefitting from a very convenient abstraction: RESTful web services.
Chances are most of the controllers in your app have a lot of shared behaviors. They find, create, and update your models, set flash messages, redirect or render templates, and set status codes. Let's take advantage of the shared behaviors to refactor our tests.
Put the following file into spec/support/controller_helpers.rb
module ControllerHelpers
extend ActiveSupport::Concern
module ClassMethods
def self.define_action(action)
define_method action do |*args, &block|
options = args.extract_options!
options[:http_method] = action
options[:controller_method] = args[0]
args[0] = [action.to_s.upcase, "#" + options[:controller_method].to_s].join(" ")
args << options
context(*args, &block)
end
end
define_action :get
define_action :post
define_action :put
define_action :delete
define_action :head
define_action :options
def it_should_flash(type, message)
it "should set the flash" do
do_request
flash[type].should eq(message)
end
end
def it_should_redirect_to(&block)
it "should redirect" do
url = instance_eval(&block)
do_request
response.should redirect_to(url)
end
end
def it_should_render_template(template)
it "should render the #{template} template" do
do_request
response.should render_template(template)
end
end
end
def do_request
params = if respond_to?(:params) then send(:params) else nil end # of story
send(example.metadata[:http_method], example.metadata[:controller_method], params)
end
end
RSpec.configure do |config|
config.include ControllerHelpers, :type => :controller
end
Now we can write our controller specs easily. For example:
require "spec_helper"
describe FoosController do
get :new do
it_should_render_template :new
end
post :create do
let(:params) { "foo" => { "bar" => "baz" } }
let(:foo) { stub }
before { Foo.stub!(:new).with("bar" => "baz").and_return(foo) }
context "successful create" do
before { foo.stub!(:save).and_return(true) }
it_should_set_flash :success, "Created foo."
it_should_redirect_to { foos_url }
end
context "failed create" do
before { foo.stub!(:save).and_return(false) }
it_should_render_template :new
end
end
end
Enjoy!
Check out our other articles on ruby on rails tips and best practices to keep your project optimized:
Application Development Process Tip: Reverse Ticket Priorities on Friday