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”

  1. Dr Nic Says:

    I'm not convinced by Rspec if you need to use “ and ” everywhere... :P

  2. Dr Nic Says:

    I'm not convinced by Rspec if you need to use \“ and \” everywhere... :P

  3. Dr Nic Says:

    The comments section unescapes text better than your blog itself... damn.

  4. David Chelimsky Says:

    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').

  5. Paul Barry Says:

    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.

  6. Nick Kallen Says:

    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.

  7. court3nay Says:

    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.

Sorry, comments are closed for this article.