[rspec-users] DRYer controller specs

Chris Hoffman
2007-04-27T13:31:25Z

Your''s is a good question.  I too have been looking for ways to DRY up
my controller tests.  The more I am starting to obey a CRUD-only
design, the more duplication I am encountering in these specs.  Right
now, I am combating this dilemma by making a bunch of methods in
spec_helper that define _context_ and _specify_ statements, such as:

def specify_should_handle_an_invalid_id_nicely(params)
  controller = "#{params[:controller].camelize}Controller"
  model = params[:controller].classify.constantize
  params[:redirect_to] ||= { :action => ''index'' }
  params[:actions] ||= [:edit, :update, :show, :destroy]

  params[:actions].to_a.each do |action|
    context "#{controller}##{action}, given an id" do
      controller_name params[:controller]

      specify "should call #{model}.find_by_id, instead of
#{model}.find" do
        model.should_receive(:find_by_id).with("the id")
        send(:process, action, :id => "the id")
      end
    end

    context "#{controller}##{action}, not given a valid id" do
      controller_name params[:controller]

      setup do
        model.stub!(:find_by_id)
      end

      specify "should redirect_to #{params[:redirect_to]}" do
        send(:process, action)
        response.should_redirect_to params[:redirect_to]
      end

      specify "should populate the flash with a message indicating
that the record wasn''t found" do
        send(:process, action)
        flash[:notice].should == "#{model} not found"
      end
    end
  end
end

This approach is obviously clunky, and I would myself like a better
way to specify controller-wide contracts, as it were.  Just as above,
any action that requires an id as a parameter should behave a certain
way, and I shouldn''t have to define this behavior in each
controller''s
specification; it should be application-wide.

-Chris

On 4/11/07, nicholas a. evans <nick at ekenosen.net>
wrote:
> So, I''ve been following the recommendations for controller specs
here:
>
http://blog.davidchelimsky.net/articles/2006/11/09/tutorial-rspec-stubs-and-mocks
>
> Most notably: a single expectation per specify block; the setup block
> contains only stubs; mock expectations each get their own specify
> block.  (I''m still using 0.8, so I haven''t gotten the
describe/it
> goodness yet.)
>
> I don''t really mind the stub/mock duplication (between setup and
the
> specifications).  What''s annoying me is the action duplication.
"get
> ''create''" is called in each specify block, sometimes
before the actual
> specification, sometimes afterward, depending on whether it''s a
mock
> expectation or a state expectation.
>
> So I played around with creating the following module and then
> extending my context with it.
>
> module MockSpecHelpers
>   def expect_that_it(msg)
>     specify msg do
>       yield
>       action
>     end
>   end
>   def then_it(msg)
>     specify msg do
>       action
>       yield
>     end
>   end
> end
>
> in the context block, I would then "def action do get
''create'' end".
> And I would replace the following specify blocks
>
>   specify "should create a new person on GET to create" do
>     Person.should_receive(:new).and_return(@person)
>     get ''create''
>   end
>
>   specify "should assign new person to template on GET to create"
do
>     get ''create''
>     assigns[:person].should_be @person
>   end
>
> with
>
>   expect_that_it "should create a new person on GET to create" do
>     Person.should_receive(:new).and_return(@person)
>   end
>
>   then_it "should assign new person to template on GET to create"
do
>     assigns[:person].should_be @person
>   end
>
> The problem is that my weak Ruby metaprogramming fu has completely
> failed me.  :-)  I can''t figure out how to get the block to be
> evaluated in a context that has access to the appropriate context
> methods (assigns, response, session, etc).  I get errors like the
> following:
>
> NameError in ''GET /controller/action/1234 should render the foobar
template''
> undefined local variable or method `response'' for
> #<Spec::Runner::ContextEvalModule:0xb6def95c>
>
> (Usually the "expect_that_it" blocks work fine, because they
aren''t
> dependent on anything that is specific to their EvalContext.)
>
> Does someone know how I could get this working?
>
> I also thought about making an alias for setup called "given" and
> instead of "action" it could be "when_action" to more
closely follow
> the "given/expect/when/then" pattern and naming conventions.
>
> Does this even look like a good idea, or is there a better way?  Would
> it make more sense to open the EvalContext rather than trying to
> extend a module?
>
> --
> Nick