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

David Chelimsky dchelimsky at gmail.com
Sun Aug 1 15:02:21 EDT 2010


On Aug 1, 2010, at 11:40 AM, Myron Marston wrote:

>> The particular issue of simple values being used in the docstrings and the examples themselves (i.e. exposed to everything in the block scope) could be handled like this:
>> 
>> shared_examples_for "blah" do |a,b|
>>   ...
>> end
>> 
>> it_should_behave_like "blah", 1, 2
> 
> Fantastic idea.  I'm sold.  I'm not sure why this simple idea didn't
> occur to me earlier :(.
> 
>> That's no different from methods that have default values for arguments:
>> 
>>   def foo(bar, baz = :default)
>> 
>> If you provide only 1 arg, all is well, but the first one is required. Here's the same idea expressed in a group:
>> 
>> shared_examples_for "foo" do
>>   unless defined?(:bar)
>>     raise "you need to supply a bar() method"
>>   end
>> 
>>   unless defined?(:baz)
>>     def baz; :default; end
>>   end
>> end
> 
> This does indeed work, to the extent that the methods the consumer
> needs to override are ones the author of the shared example ground had
> in mind, and coded as such.  This isn't an issue when the shared
> example group and the consuming code are in the same code base.  But
> the idea has been brought up that shared example groups could be
> provided by a library for users to use to enforce a contract of some
> class or object they write that the library interacts with.  I think
> it's likely that library authors won't declare their helper methods
> using the "unless defined?" idiom, because they can't anticipate all
> the needs of their users, and they probably aren't even aware that
> they have to declare their methods this way to allow them to be
> overridden.

<disclaimer>I kind of jumped around from one part of this thread to the other - apologies if my responses seem to lack cohesion</disclaimer>

That would be the tricky gotta RTFM part - I'd rather have that burden on the shared group/library author than its consumer.

>  Suddenly it's _impossible_ for consumers of the shared
> example group to override any of the helper methods.
> 
> I _love_ how flexible ruby is, and the fact that every method can be
> overridden, without the original author of the a method having to do
> anything special to allow it.  Your suggestion above seems (to me
> anyway) to be more in line with a language like C#, where methods are
> not overriddable by default, and developers have to use the virtual
> keyword to make them so.

Seems like your mental model is that of a customization block being a subclass or re-opening of the shared block. What you say makes sense in that model, but that's not the same model I have.

If we were going to go with the subclass model, I think we'd be best served by going all the way there. So this structure:

describe "foo" do
  it_behaves_like "bar", "with baz == 3" do
    def baz; 3 end
  end
end

Would result in:

foo # top level group
  it behaves like bar # nested group generated by it_behaves_like using "bar" in the report
    with baz == 3 # 3rd level nested group generated using the customization block

This would make the whole relationships between things much more transparent. I don't love this idea either, but we're searching for balance here. At least I am :)

> So, all of that is just to say that I'm still in favor of eval'ing the
> customization block last.  To me, the primary need for eval'ing the
> customization block first was to allow it define class helper methods
> that the shared example group could use to interpolate into doc
> strings, and this need is solved much more elegantly with David's
> suggestion.

Assuming that can work. I've taken a closer look and getting that to work would take some serious re-architecting that I'm not sure is a good idea. Consider the code as it is now:

http://github.com/rspec/rspec-core/blob/cc72146205fb93ca11e1f290d3385151b51181ad/lib/rspec/core/example_group.rb#L61

I've restructured it a bit after some of this conversation, but right now the customization block is still eval'd last. I think this code is very easy to grok for a reasonably advanced Rubyist (i.e. if you can get past module_eval(<<-END_RUBY), then the content of the String is no problem), but if we start adding gymnastics to support various combinations of nice-to-haves, then this code will quickly become harder to read.

In my experience with RSpec, readability/simplicity of the internals _does_ matter to end users, not just contributors and maintainers, because many want to understand what's going on under the hood. That is a strong motivator for me to keep things exactly as they are now (simple and readable).

In terms of end-users, the consumer of the shared group would not need to be any different in either of these two scenarios:

shared_examples_for "foo" do
  # with customization_block eval'd before
  unless defined?(:bar)
    raise "you need to supply a bar() method"
  end
end

shared_examples_for "foo" do
  # with customization_block eval'd after
  def bar
    raise "you need to supply a bar() method"
  end
end

In either case, this will get you the same error:

it "does something" do
  it_behaves_like "bar"
end

> I like eval'ing it last so that helper methods can be
> overridden, _without_ anything special being done in the shared
> example group.

I can appreciate that but I'd rather have burden placed on the shared group author than its consumer.

Cheers,
David

> Of course, if there are other things that will only work by eval'ing
> the block first, then I'm completely fine with it--I'm just not seeing
> the need right now.
> 
> Myron
> _______________________________________________
> rspec-users mailing list
> rspec-users at rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users



More information about the rspec-users mailing list