[rspec-devel] huge setup methods? I think I'm on the wrong path

Matt Pelletier matt at eastmedia.com
Wed Jan 24 17:40:16 EST 2007


On Jan 24, 2007, at 4:36 PM, David Chelimsky wrote:

> On 1/24/07, Jens-Christian Fischer <jcfischer.lists at gmail.com> wrote:
>>>
>>> If the view needs to ask a lot of questions, then your setup is  
>>> going
>>> to look something like that. You can, though, clean things up a  
>>> little
>>> bit:
>>
>>> [... snip ...]
>>> This will cause the mock to ignore calls that it isn't interested  
>>> in,
>>> so you won't get failures by sending messages. If, however, your  
>>> view
>>> has any trainwrecks in it (user.address.zipcode), then you'd have to
>>> supply something specific to return for address.
>>>
>>
>> Yes - there are quite a few trainwrecks :-) That's the first time I
>> heard that - it sounds like there are some negative cononations with
>> that - true?

A few comments on the above. These topics have led to long  
discussions here and we've come up with a few idioms to handle the  
various less-than-conveniences. Big thanks to Wilson Bilkovich and  
Bryan Helmkamp for crafting this together.

We have gotten into the practice of writing predicate methods on  
models (in Rails). These accomplish a few things:

1. Keep the Demeterites happy
2. Make code easier to read
3. Make code easier to spec

A simple example:

class Client < ActiveRecord::Base
   has_many :people
end

class Person < ActiveRecord::Base
   belongs_to :person

   def client_name
     client.name
   end
end

Now instead of:

client = mock('client')
client.stub!(:name).and_return('fred')

person = mock('person')
person.stub!(:client).and_return(client)

You can just do:

person = mock('person')
person.stub!(:client_name).and_return('fred')

The level of convenience increases proportionally to the number of  
association proxy calls (aka the Pit of Despair) you would normally  
be required to make.

This does mean a small amount of extra code in your app, but really  
not too much, and probably less than you'd be adding to your specs to  
stub around it.

We wrote a plugin that will create some of these for you  
automatically that wrap Rails' association proxies. It's part of a  
set of other convenient tools and Rails 'hacks' (hello Pot, this is  
Kettle).

http://svn.eastmedia.com/svn/code/plugins/model_extensions/lib/ 
associations.rb

If you want to install the plugin directly, use:

$ ./script/plugin install http://svn.eastmedia.com/svn/code/plugins/ 
model_extensions

We also use some of the delegator methods built into Ruby for cases  
where we want to directly reference an object's method. These are  
roughly the same as Rails' 'delegate' method, which is basically a re- 
implementation of this existing functionality, they're just not as  
easy to understand at first glance.

The above model code could be written as:

class Client < ActiveRecord::Base
   has_many :people
end

class Person < ActiveRecord::Base
   belongs_to :person
   # The following gives you @person.client_name.
   # You can omit the last param if you want to use @person.name,  
assuming it won't conflict
   def_delegator :client, :name, :client_name
end

As for long-winding-road setups, even with this convenience you are  
still going to run into them. What we've done is define a module that  
has a method with the innards of #setup, and you just include the  
module in your context, and call the method name from inside setup.  
This is especially useful if you need similar setups from multiple  
contexts. You can either define the module at the top of your  
_spec.rb or put it in fixtures/ somewhere.

module PersonSetup
   def setup_person
     @client = Client.create! :name => "Fredder"
     @person = @client.build_person :first_name => "Chuck"
   end
end

context "A person being assigned a task" do
   include PersonSetup
   setup do
     setup_person
   end
end

If the method you need is simple and you're ok with just throwing the  
def X at the top of the _spec.rb, we've done that too.

Finally... there are some mocking / stubbing convenience tricks you  
can do. We have added the following to our spec_helper.rb class.  
Thanks to Wilson for his work on this. I modified it to allow for  
additional stubs to be defined in the method call using a simple  
stubbing method. I don't think it's 'juuuust' right yet in the  
Goldilocks sense of the word, but it works for now and saves lots o'  
lines.

context "A person completing a task"
   setup do
     mock_model(:client, :name => 'Heys')
     mock_model(:person, :client => @campaign, :first_name =>  
"Joe", :tasks => [])
   end
# ...
end

The following code can be crammed into spec_helper.rb inside  
EvalContext. I wrote it here just wrapped in a class_eval. Wilson  
originally posted mock_model on his blog (http://metaclass.org/ 
2006/12/22/making-a-mockery-of-activerecord). This is a slightly  
modified version.

Spec::Rails::class EvalContext.class_eval do

       def mock_model(name, stubs = {})
         name = name.to_s
         m = mock(name)
         instance_variable_set("@#{name}", m)
         id = rand(10_000)
         m.stub!(:id).and_return(id)
         m.stub!(:to_param).and_return(id)
         m.stub!(:new_record?).and_return(false)
         klass = name.singularize.camelize
         m.send(:__mock_handler).instance_eval <<-CODE
           def @target.is_a?(other)
             other == #{klass}
           end
           def @target.class
             #{klass}
           end
         CODE
         add_stubs(m,stubs)
         yield m if block_given?
         m
       end

       # Will add stubs to a new or existing object.
       # Passing a symbol or string will create a new mock, passing  
anything else
       # will create a new mock and add stubs to that
       def add_stubs(object, stubs = {})
         m = object.class.in?([String, Symbol]) ? mock(object.to_s) :  
object
         stubs.each {|k,v| m.stub!(k).and_return(v)}
         m
       end
end

Hope that helps.

Matt


>
> Google "Law of Demeter". Note that it is called a Law, which makes it
> sound absolute. Some refer to it lovingly as the "Suggestion of
> Demeter" (I believe Fowler first wrote that). It is a useful guideline
> that should be applied when you're feeling pain (which it seems that
> you are in this case).
>
> Cheers,
> David
> _______________________________________________
> rspec-devel mailing list
> rspec-devel at rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-devel

------------------
Matt Pelletier
http://www.eastmedia.com -- EastMedia
http://www.informit.com/title/0321483502 -- The Mongrel Book
http://identity.eastmedia.com -- OpenID, Identity 2.0





More information about the rspec-devel mailing list