[rspec-users] Evaluating shared example customisation block before shared block

Ashley Moran ashley.moran at patchspace.co.uk
Sat Jul 31 15:42:18 EDT 2010


On 31 Jul 2010, at 7:06 PM, Myron Marston wrote:

> Good point--I hadn't thought of that.  The one issue I see with it is
> that the author of the shared example group may not have knowledge of
> which helper methods consumers will need to override.  So he/she
> either defines all helper methods that way, or guesses about which
> ones to define that way (and potentially guesses wrong).

I wonder if this will happen in practice?  I can't think of an example off the top of my head, which isn't to say it won't matter, but it may be better done pull-based, when the need arises.


> If we go the route of having the customization block evaluated first,
> then I like the idea, but I'm generally wary of adding more DSL
> methods to RSpec.  I think we should be careful to only add new DSL
> methods that many people will find useful.  If you find it useful,
> it's very easy to use it in your project without it being part of
> RSpec: just define default_helper in a module, and use config.extend
> YourModule in the RSpec.configuration block.  (Note that I'm _not_
> against adding this to RSpec: I just want to be sure we don't add a
> bunch of DSL methods that have limited usefulness.)

This is a fair point.  I'm going to the effort of implementing this spike in rspec-core itself because I *really* want to see if there is value in re-usable shared examples (my own, admittedly small, side-project already suggests there is).  But I'm fairly sure it's not a pattern in wide use, at least not with Ruby testing libraries.


> Looking back at the initial example that prompted the thread, it looks
> to me like the primary use case for evaluating the customization block
> first is so that you can parameterize the shared example group's
> example descriptions.  (There may be other use cases for defining a
> class-level helper methods, but none springs to mind).  I also do this
> frequently.  Often times I have something like this:
> 
> [:foo, :bar, :baz].each do |method|
>  it "does something for #{method}" do
>     subject.send(method).should ...
>  end
> end
> 
> In this case I'm using the method parameter at the class level (to
> interpolate into the description string) and at the instance level
> (within the example itself).
> 
> If we evaluated the customization block first, it would allow this,
> but you'd have to define both an instance and class helper:
> 
> it_should_behave_like "something" do
>  def self.method_name; :foo; end
>  def method_name; :foo; end
> end
> 
> I think this is a clunky way to essentially pass a parameter to the
> shared example group.

Funny you mention this.  While I've been working on my patch[1] I came to the same conclusion.  This (heavily trimmed down - I may have broken it cutting bits out for email purposes) example demonstrates it:

  module RSpec::Core
    describe SharedExampleGroup::Requirements do
      it "lets you specify requirements for shared example groups" do
        shared_examples_for("thing") do
          require_class_method :configuration_class_method, "message"
        
          it "lets you access #{configuration_class_method}s" do
            self.class.configuration_class_method.should eq "configuration_class_method"
          end

          it "lets you access #{configuration_class_method}s" do
            configuration_class_method.should eq "configuration_class_method"
          end
        end

        group = ExampleGroup.describe("group") do
          it_should_behave_like "thing" do
            def self.configuration_class_method
              "configuration_class_method"
            end 
          end
        end
      
        group.run_all.should be_true
      end
    end
  end

However, I found a serious issue with class methods, namely that they are being defined in a persistent class, not a transient ExampleGroup subclass.  I haven't investigated this yet*, but I've left a pending spec at the appropriate point.

* Random thought after seeing your code: using `class << self; end` over `def self.x; end` may be a partial answer?


> Better would be something like this:
> 
> it_should_behave_like "something" do
>  providing :method_name, :foo
> end
> 
> The instance of the shared example group provides :foo as the value of
> the method_name parameter.  providing simply defines a class and an
> instance helper method with the given value.
> 
> I've written up an untested gist with a start for the code that would
> implement this:
> 
> http://gist.github.com/502409

Thanks for writing this - it's an interesting piece of code.  Certainly it also gets around the class/instance scope divide.  But I don't think it can enforce that the parameter is provided?  One of my hopes is to make the errors completely self documenting.

Aside: one design decision I've made is to make every error due to a missing requirement fail at the example level, rather than abort the whole spec run.  This is because RSpec-formatted requirements are MUCH easier to read than a random stacktrace in a terminal.  To do this, you need to specify the class method requirement (comments added for explanatory purposes):

  def require_class_method(name, description)
    if respond_to?(name)
      # We have the class method, so alias it in the instance scope
      define_method(name) do |*args|
        self.class.send(name, *args)
      end
    else
      # We don't have the class method, so fail all the examples,
      # but provide a class-level method so the example definitions
      # doesn't fail, and break the run
      before(:each) do
        raise ArgumentError.new(
          %'Shared example group requires class method :#{name} (#{description})'
        )
      end
      self.class.class_eval do
        define_method(name) do |*args|
          %'<missing class method "#{name}">'
        end
      end
    end
  end

> I think there's value in evaluating the customization block first and
> value in evaluating it last.  We can get the best of both worlds if we
> limit what's evaluated first to a subset (say, a few DSL methods, and
> maybe all class method definitions), extract it, and evaluate that
> first, then evaluate the shared block first, then evaluate the
> customization block.  The gist demonstrates this as well.  This may
> confuse people, but it does give us the best of both worlds, I think.

I think it's fair to say this is not a simple one to resolve :)

Maybe David has ideas on how to reconcile everything?

Cheers
Ash


[1] http://github.com/ashleymoran/rspec-core/tree/issue_99_shared_example_block_ordering


-- 
http://www.patchspace.co.uk/
http://www.linkedin.com/in/ashleymoran





More information about the rspec-users mailing list