Mocking ActionController for fast controller specs.
08 Feb 2012, by Ian Leitch

This is the first post on my new blog, awesome. Right, lets make our controller specs sub-second fast.

Corey Haynes and a few other people have been talking recently about putting more of a focus on applying Domain Modelling practices to our Rails apps. The vast majority of developers will already be doing this to some extent, but the recent focus has been on the extend to which you separate your app from Rails. In hindsight, shame on us from not doing this from the start.. but hey, you live and learn.

In a somewhat new app i’ve been extracting as much logic as possible from my Models and Controllers and placing them in classes or modules that live in the lib directory. Apart from the obvious benefit of neat encapsulation, it has the added bonus of allowing your unit tests for those modules and classes to be blazingly fast, as demonstrated by Corey.

One remaining issue that was still bugging me was that despite the vast majority of logic from my controllers having been moved elsewhere, I still had to load my Rails environment to test what was left of them. I use rspec, and with it a lot of the Rails integration for testing my controllers. Where we’re going though, we wont be able to use any of that.

Instead of treating the Controller as a Controller, we’re going to treat it just like any old class. These are supposed to be unit tests after all, right?

First things first, we need a simple ActionController::Base mock:

 1 module ActionController
 2   class Base
 3     class << self
 4       attr_reader :before_filters
 5       attr_reader :skipped_before_filters
 6     end
 7 
 8     def self.before_filter(*args)
 9       @before_filters ||= []
10       @before_filters << args
11     end
12 
13     def self.skip_before_filter(*args)
14       @skipped_before_filters ||= []
15       @skipped_before_filters << args
16     end
17 
18     def self.layout(*args); end
19     def self.protect_from_forgery; end
20     def self.respond_to(*args); end
21     def render(*args); end
22     def redirect_to(*args); end
23     def render_template(*args); end
24     def params; {} end
25   end
26 end

Ignore what’s going on inside before_filter and skip_before_filters for now, i’ll get to that in a bit.

Here is a pretty standard controller spec, sprinkled with some rspec-rails integration helpers:

 1 describe Portal::CouponsController, 'update', :type => :controller do
 2   let(:coupon) { stub(:id => 1, :update_attributes => true) }
 3   let(:organization) { stub(:id => 1, :coupons => stub(:find => coupon)) }
 4 
 5   before do
 6     Portal::CouponPresenter.stub(:new)
 7     Organization.stub(:find_by_id => organization)
 8     controller.stub(:authenticate_user!)
 9     controller.stub(:authorize!)
10   end
11 
12   def do_put(coupon_params = {})
13     put :update, :organization_id => organization.id, :id => coupon.id, :coupon => coupon_params, :subdomain => 'portal'
14   end
15 
16   it 'updates the attributes on the coupon' do
17     coupon_params = {:name => '10% off'}.stringify_keys
18     coupon.should_receive(:update_attributes).with(coupon_params)
19     do_put(coupon_params)
20   end
21 
22   it 'redirects to the show action after the coupon is updated' do
23     coupon.stub(:update_attributes => true)
24     do_put
25     response.should redirect_to :action => :show
26   end
27 
28   it 'renders the edit action if the update fails' do
29     coupon.stub(:update_attributes => false)
30     do_put
31     response.should render_template :edit
32   end
33 
34   it 'presents the coupon if the update fails' do
35     coupon.stub(:update_attributes => false)
36     Portal::CouponPresenter.should_receive(:new).with(coupon)
37     do_put
38   end
39 
40   it 'assigns the presenter to @coupon' do
41     coupon.stub(:update_attributes => false)
42     Portal::CouponPresenter.stub(:new => coupon)
43     do_put
44     assigns[:coupon].should == coupon
45   end
46 
47   it 'flashes that the coupon has been updated' do
48     coupon.stub(:update_attributes => true)
49     do_put
50     flash.notice.should == 'Coupon updated.'
51   end
52 end

And here is how to looks without any dependency on rspec-rails:

 1 describe Portal::CouponsController, 'update' do
 2   let(:coupon) { stub(:id => 1, :update_attributes => nil) }
 3   let(:organization) { stub(:id => 1, :coupons => stub(:find => coupon)) }
 4   let(:params) { { :organization_id => organization.id, :id => coupon.id, :coupon => {:name => '10% off'} } }
 5 
 6   before do
 7     subject.stub(:params => params)
 8     subject.stub(:organization => organization)
 9     subject.stub(:coupon_model => coupon)
10     subject.stub(:flash => stub(:notice= => nil))
11     Portal::CouponPresenter.stub(:new)
12   end
13 
14   it 'updates the attributes on the coupon' do
15     coupon.should_receive(:update_attributes).with(:name => '10% off')
16     subject.update
17   end
18 
19   it 'redirects to the show action after the coupon is updated' do
20     coupon.stub(:update_attributes => true)
21     subject.should_receive(:redirect_to).with(:action => :show)
22     subject.update
23   end
24 
25   it 'renders the edit action if the update fails' do
26     coupon.stub(:update_attributes => false)
27     subject.should_receive(:render).with(:action => :edit)
28     subject.update
29   end
30 
31   it 'presents the coupon if the update fails' do
32     coupon.stub(:update_attributes => false)
33     Portal::CouponPresenter.should_receive(:new).with(coupon)
34     subject.update
35   end
36 
37   it 'assigns the presenter to @coupon' do
38     coupon.stub(:update_attributes => false)
39     Portal::CouponPresenter.stub(:new => coupon)
40     subject.update
41     subject.should have_assign(:coupon, coupon)
42   end
43 
44   it 'flashes that the coupon has been updated' do
45     coupon.stub(:update_attributes => true)
46     subject.flash.should_receive(:notice=).with('Coupon updated.')
47     subject.update
48   end
49 end

The most obvious changes are that we know call the action method directly instead of using the route, stubs are still made on the controller instance, but that’s now provided for us by rspec as subject.

There’s room for improvement here, notably with flashes. I’ll probably add a matcher for those, i.e subject.should flash_notice('...').

Matchers for before_filter and friends actually becomes much simpler to implement as they’re now just stored in an array which we’ve exposed with an attr_reader.

 1 RSpec::Matchers.define :have_before_filter do |filter_name, options|
 2   match do |actual|
 3     has_filter = false
 4     actual.class.before_filters.each do |filters|
 5       filter_options = filters.last if filters.last.is_a?(Hash)
 6       if filters.include? filter_name
 7         has_filter = true
 8         if options && options[:only]
 9           has_filter = filter_options[:only].sort == options[:only].sort
10         end
11       end
12     end
13     has_filter
14   end
15 end

1 describe MyController do
2   subject.should have_before_filter(:some_filter)
3 end
4 
5 describe MyController, 'some_filter' do
6   it '...' do
7     subject.send(:some_filter)
8   end
9 end

You'll need to extend the matcher to support other options as needed. I generally try and avoid using :except however.

How long does it take to run all my controller specs now?

[master] $ time rspec spec/standalone/app/controllers/
..................................................................................................................................................................................

Finished in 0.24854 seconds
178 examples, 0 failures, 0 pending

real  0m0.783s
user  0m0.624s
sys   0m0.121s

Can’t complain with those numbers!

If you’re still twiddling your thumbs waiting for Rails to load for your unit tests, I highly recommend giving this a try. Fast specs have made a huge impact on my desire to write tests and achieve 100% coverage.

blog comments powered by Disqus