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

Matt Pelletier matt at eastmedia.com
Thu Jan 25 22:30:57 EST 2007


On Jan 25, 2007, at 6:17 AM, aslak hellesoy wrote:

> This is good stuff. I'll add it to spec/rails and the  
> rspec_resource generator!
>

Great! Thanks. I just sent an updated version to this thread. I'll  
include it here as well.

       # Adds stubs on a new or existing object. A new mock will be  
created if the
       # first param is a Symbol or String.
       def add_stubs(object, stubs = {})
         m = [String, Symbol].include?(object.class) ? mock 
(object.to_s) : object
         stubs.each {|k,v| m.stub!(k).and_return(v)}
         m
       end

       # Creates a mock for an ActiveRecord model and places it in an  
instance variable.
       # Automatically creates stubs for commonly called methods such  
as #id, #to_param,
       # and #new_record?. You can pass a hash of stubs you would  
like created (these
       # will override the defaults). It also accepts an optional block.
       def mock_model(name, stubs = {})
         name = name.to_s
         m = mock(name)
         instance_variable_set("@#{name}", m)
         id = stubs[:id] || rand(10_000)
         stubs.reverse_merge! :id => id, :to_param =>  
id, :new_record? => 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


> Aslak
>
> On 1/24/07, Matt Pelletier <matt at eastmedia.com> wrote:
>>
>> 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
>>
>>
>>
>> _______________________________________________
>> rspec-devel mailing list
>> rspec-devel at rubyforge.org
>> http://rubyforge.org/mailman/listinfo/rspec-devel
>>
> _______________________________________________
> 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