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

aslak hellesoy aslak.hellesoy at gmail.com
Thu Jan 25 06:17:00 EST 2007


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

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
>


More information about the rspec-devel mailing list