skinny controllers, skinnier controller specs
Courtenay : August 24th, 2007
So, you're happily using mocks to remove the database from your skinny™ controller.
The code has been hacked on by about four different people and looks something like
describe CategoriesController, "showing a record" do
before do
@store = mock_model(Store, :categories => mock('categories proxy'))
@product = mock_model(Product)
@store.categories.stub!(:find_by_permalink).and_return @product
@product.stub!(:name).and_return('foo')
end
it "should show successfully" do
get :show
response.should render_template('show')
end
it "should load one record" do
@store.products.should_receive(:find_by_permalink).with('1').and_return @product
get :show
end
end
To be honest, it's pretty nasty, and with rSpec, if it feels nasty it's probably wrong. The controller is quite simple
class CategoriesController < ApplicationController
before_filter :load_store
protected
def load_store
@store = Store.find(session[:store_id])
end
public
def show
@category = @store.categories.find_by_permalink(params[:id])
end
def edit
@category = @store.categories.find_by_permalink(params[:id])
end
def update
@category = @store.categories.find_by_permalink(params[:id])
@category.update_attributes(params[:category])
end
end
Now, there are two ways of DRYing up this. They both involve a "find_category" method. The holy war involves whether you load the data in a before_filter or explicitly set @category in each action. I think the first is much cooler.
class CategoriesController < ApplicationController
before_filter :find_category, :only => [ :show, :edit, :update ]
protected
def store
@store ||= Store.find(session[:store_id])
end
def find_category
@category = store.categories.find_by_permalink(params[:id])
end
public
def show
end
def edit
end
def update
@category.update_attributes(params[:category])
end
end
In the new spec, we can do something like this:
describe CategoriesController, "showing a record" do
before do
controller.stub!(:find_store)
controller.stub!(:find_category)
controller.instance_variable_set(:@category, mock_model(Category)
end
it "should show successfully" do
get :show
response.should render_template('show')
end
it "should load one record" do
controller.should_receive(:find_category)
get :show
end
end
describe CategoriesController, "finding a record" do
before do
@store = mock_model(Store)
controller.stub!(:store).and_return(@store)
end
it "should find a record by permalink" do
controller.stub!(:params).and_return({ :id => '1' })
@store.should_receive(:find_by_permalink).with('1')
controller.send(:find_category)
end
end
First, we test the "should show.." logic. Then, in a different context, we test that the "find" works as advertised.
Got a better way?
7 Responses to “skinny controllers, skinnier controller specs”
Sorry, comments are closed for this article.
August 24th, 2007 at 09:35 AM
I'm not convinced by Rspec if you need to use “ and ” everywhere... :P
August 24th, 2007 at 09:36 AM
I'm not convinced by Rspec if you need to use \“ and \” everywhere... :P
August 24th, 2007 at 09:36 AM
The comments section unescapes text better than your blog itself... damn.
August 26th, 2007 at 04:46 AM
Courtenay - nice example.
Dr Nic - if you're referring to 'and_return' it's because we couldn't make 'return' a method and 'returns' didn't make sense w/ the command feel ('should receive') as opposed to a narrative feel ('receives' or 'expects').
August 26th, 2007 at 06:39 PM
The only thing the that you need to verify in the spec for the show action, besides that it is successfully and renders the show template, is that the @category gets set. This spec verifies that find_category gets called, but doesn't check to see if @category get assigned. find_category is just an implementation detail, the spec shouldn't care whether that gets called or not, just that the @category gets set.
August 26th, 2007 at 09:13 PM
I dislike this excessive mocking in testing the Controller layer. This is a religious debate so I realize so I'll just move onto the next topic.
What the heck are you doing testing Protected methods? Again, you mock 'find_by_permalink', tightly coupling your test to the implementation. A straightforward refactoring might be:
Category.findby_param()--and similarly, calling toparam in your tests (as you url_for would typically). (incidentally, this is a pattern used in sample.caboo.se, and it's nice).
This kind of refactoring should not break tests.
August 27th, 2007 at 10:54 AM
Nick: Yes, I forgot to mention that findby_permalink is what I called findby_param. The implementation is not actually using the rails autogenerated helper method.