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

732 views
Skip to first unread message

Ashley Moran

unread,
Jul 30, 2010, 6:13:13 AM7/30/10
to rspec-users
Hi

I finally looked into why this is not currently possibly in RSpec 2 (beta 19):

shared_examples_for "Etymology" do
describe "The etymology of foo" do
it "is followed by #{after_foo}" do
# ...
end
end
end

describe "foo", focus: true do
it_should_behave_like "Etymology" do
def self.after_foo
"bar"
end
end
end

It's because of the current implementation of ExampleGroup.define_shared_group_method, which evaluates the shared example block before the customisation block:

shared_group = describe("#{report_label} \#{name}", &shared_block)
shared_group.class_eval(&customization_block) if customization_block

(This is behaviour I found surprising.)

However, with a little more metaprogramming jiggery-pokery, you can have them evaluated in the other order:

module RSpec
module Core
class ExampleGroup
# ...

def self.define_shared_group_method(new_name, report_label=nil)
report_label = "it should behave like" unless report_label
module_eval(<<-END_RUBY, __FILE__, __LINE__)
def self.#{new_name}(name, &customization_block)
shared_block = world.shared_example_groups[name]
raise "Could not find shared example group named \#{name.inspect}" unless shared_block

compound_block = lambda do |*args|
module_eval &customization_block if customization_block
module_eval &shared_block
end

shared_group = describe("#{report_label} \#{name}", &compound_block)
shared_group
end
END_RUBY
end

# ...
end
end
end

Would this be a useful improvement to RSpec 2? Any opinions on the order of the block evaluation for shared examples?

Cheers
Ash

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

_______________________________________________
rspec-users mailing list
rspec...@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

David Chelimsky

unread,
Jul 30, 2010, 10:03:41 AM7/30/10
to rspec-users

Or ...

def self.define_shared_group_method(new_name, report_label=nil)
report_label = "it should behave like" unless report_label
module_eval(<<-END_RUBY, __FILE__, __LINE__)
def self.#{new_name}(name, &customization_block)
shared_block = world.shared_example_groups[name]
raise "Could not find shared example group named \#{name.inspect}" unless shared_block

describe "#{report_label} \#{name}" do


module_eval &customization_block if customization_block
module_eval &shared_block
end

end
END_RUBY
end

> Would this be a useful improvement to RSpec 2?

Yes

> Any opinions on the order of the block evaluation for shared examples

Makes perfect sense to me. Wanna make a patch with an additional scenario in the cuke?

David Chelimsky

unread,
Jul 30, 2010, 10:10:20 AM7/30/10
to David Chelimsky, rspec-users

Actually - maybe I spoke to soon. Ordering things this way would mean that you couldn't do this:

shared_examples_for Enumerable do
def enumerable
raise "you must provide an enumerable method that returns the object which you're specifying should behave like Enumerable"
end
it "..." { .. }
end

Although, if you were going to do that, I guess you could do this:

shared_examples_for Enumerable do
unless defined?(:enumerable)
raise "you must provide an enumerable method that returns the object which you're specifying should behave like Enumerable"
end
it "..." { .. }
end

Maybe that, or a DSL that wraps that, is the better way, so we can get the best of both worlds?

shared_examples_for Enumerable do
require_instance_method :foo, "gotta have foo instance method"
require_class_method :foo, "gotta have foo class method"
require_instance_variable "@foo", "gotta have an instance variable named @foo"
it "..." { .. }
end

Thoughts?

Wincent Colaiuta

unread,
Jul 30, 2010, 11:17:23 AM7/30/10
to rspec-users
El 30/07/2010, a las 16:10, David Chelimsky escribió:

> Actually - maybe I spoke to soon. Ordering things this way would mean that you couldn't do this:
>
> shared_examples_for Enumerable do
> def enumerable
> raise "you must provide an enumerable method that returns the object which you're specifying should behave like Enumerable"
> end
> it "..." { .. }
> end
>
> Although, if you were going to do that, I guess you could do this:
>
> shared_examples_for Enumerable do
> unless defined?(:enumerable)
> raise "you must provide an enumerable method that returns the object which you're specifying should behave like Enumerable"
> end
> it "..." { .. }
> end
>
> Maybe that, or a DSL that wraps that, is the better way, so we can get the best of both worlds?
>
> shared_examples_for Enumerable do
> require_instance_method :foo, "gotta have foo instance method"
> require_class_method :foo, "gotta have foo class method"
> require_instance_variable "@foo", "gotta have an instance variable named @foo"
> it "..." { .. }
> end
>
> Thoughts?

I think the DSL would probably be over-engineering.

One of the purposes the DSL would fulfill is to alert the user if he/she forgets to provide some required support methods or variables, but you already get those alerts "for free" in the form of failing specs and NoMethodErrors etc, so I don't think that really justifies it.

The other purpose of the DSL would be to explicitly list the "dependencies" of the shared example group to someone who's scanning it. Again, I'm not sure if it's really justified, given that a much simpler solution already exists:

shared_examples_for Enumerable do
# requires:
# - instance method: foo
# - class method: foo
# - instance variable: @foo

it "..." { ... }
end

(I'm a big fan of doing the simplest thing that could possibly work.)

Cheers,
Wincent

Ashley Moran

unread,
Jul 30, 2010, 11:57:52 AM7/30/10
to rspec-users

On Jul 30, 2010, at 3:10 pm, David Chelimsky wrote:

> Maybe that, or a DSL that wraps that, is the better way, so we can get the best of both worlds?
>
> shared_examples_for Enumerable do
> require_instance_method :foo, "gotta have foo instance method"
> require_class_method :foo, "gotta have foo class method"
> require_instance_variable "@foo", "gotta have an instance variable named @foo"
> it "..." { .. }
> end
>
> Thoughts?

Actually, I *much* prefer this, as it makes it explicit what the host spec must provide in order to have the shared behaviour. (Wincent's email has just popped up so consider this a response to his message too.) I lean towards making things fail as early and explicitly as possible, as it reduces the total worldwide developer head-scratching time. InvalidSharedExampleUsageError("Must provide instance method :foo") or some such wins for me over NoMethodError("undefined method foo on <#Class ...>"). There should be no _requirement_ to use this DSL though, so the key would be to make sure it doesn't muddy the RSpec code.

This is probably something that could be solved in code quicker than debate. It's a yak I'm happy to shave before getting back to my own project as it impacts what I'm working on ... want me to have a look at it this evening, David? I can fork rspec-core and play around with the idea.

Regards

David Chelimsky

unread,
Jul 30, 2010, 12:00:51 PM7/30/10
to rspec-users

On Jul 30, 2010, at 10:57 AM, Ashley Moran wrote:

>
> On Jul 30, 2010, at 3:10 pm, David Chelimsky wrote:
>
>> Maybe that, or a DSL that wraps that, is the better way, so we can get the best of both worlds?
>>
>> shared_examples_for Enumerable do
>> require_instance_method :foo, "gotta have foo instance method"
>> require_class_method :foo, "gotta have foo class method"
>> require_instance_variable "@foo", "gotta have an instance variable named @foo"
>> it "..." { .. }
>> end
>>
>> Thoughts?
>
> Actually, I *much* prefer this, as it makes it explicit what the host spec must provide in order to have the shared behaviour. (Wincent's email has just popped up so consider this a response to his message too.) I lean towards making things fail as early and explicitly as possible, as it reduces the total worldwide developer head-scratching time. InvalidSharedExampleUsageError("Must provide instance method :foo") or some such wins for me over NoMethodError("undefined method foo on <#Class ...>"). There should be no _requirement_ to use this DSL though, so the key would be to make sure it doesn't muddy the RSpec code.
>
> This is probably something that could be solved in code quicker than debate. It's a yak I'm happy to shave before getting back to my own project as it impacts what I'm working on ... want me to have a look at it this evening, David? I can fork rspec-core and play around with the idea.

By all means.

> Regards
> Ash

David Chelimsky

unread,
Jul 30, 2010, 11:47:55 AM7/30/10
to rspec-users

That would only work if people read the docs :)

The programatic approach would warn the user when they try to do something.

I'm not sold on the DSL at this point, but I do like the idea of having a library help the developer do the right thing. If I provide spec extensions for a library (other than RSpec in this case) that include a shared group that "you can use to spec your foos to make sure they comply with this API" (or whatever), then this sort of thing can be very helpful. Make sense?

Ashley Moran

unread,
Jul 30, 2010, 12:21:26 PM7/30/10
to rspec-users

On Jul 30, 2010, at 4:47 pm, David Chelimsky wrote:

> If I provide spec extensions for a library (other than RSpec in this case) that include a shared group that "you can use to spec your foos to make sure they comply with this API" (or whatever), then this sort of thing can be very helpful. Make sense?

That's exactly what I want to do with the framework/library code I'm (slowly) extracting =)

_______________________________________________

Ashley Moran

unread,
Jul 30, 2010, 5:58:12 PM7/30/10
to rspec-users

On 30 Jul 2010, at 5:00 PM, David Chelimsky wrote:

> By all means.

I've started on that and filed a ticket[1].

One question I have, is I keep calling the Example Group that uses a shared block the "host group". Is there already a name for it? I never know what to use, and I'm not sure "host group" describes it accurately anyway.

Cheers
Ash

[1] http://github.com/rspec/rspec-core/issues/issue/99

_______________________________________________

Myron Marston

unread,
Jul 30, 2010, 7:56:35 PM7/30/10
to rspec...@rubyforge.org
I may be the only one who finds this useful, but I think there's value
in evaluating the customization block after the shared example group
block. It allows the shared example group to provide a default
implementation of a helper method, and then an instance of the shared
behavior to override the helper method if appropriate. If you
evaluate the customization block before the shared example group
block, the default implementation wins out, and you have no way to
override helper methods in an instance of a shared example group.

On Jul 30, 2:58 pm, Ashley Moran <ashley.mo...@patchspace.co.uk>
wrote:


> On 30 Jul 2010, at 5:00 PM, David Chelimsky wrote:
>
> > By all means.
>
> I've started on that and filed a ticket[1].
>
> One question I have, is I keep calling the Example Group that uses a shared block the "host group".  Is there already a name for it?  I never know what to use, and I'm not sure "host group" describes it accurately anyway.
>
> Cheers
> Ash
>
> [1]http://github.com/rspec/rspec-core/issues/issue/99
>

> --http://www.patchspace.co.uk/http://www.linkedin.com/in/ashleymoran
>
> _______________________________________________
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

David Chelimsky

unread,
Jul 30, 2010, 8:10:07 PM7/30/10
to rspec-users
On Jul 30, 2010, at 6:56 PM, Myron Marston wrote:

> On Jul 30, 2:58 pm, Ashley Moran <ashley.mo...@patchspace.co.uk>
> wrote:
>> On 30 Jul 2010, at 5:00 PM, David Chelimsky wrote:
>>
>>> By all means.
>>
>> I've started on that and filed a ticket[1].
>>
>> One question I have, is I keep calling the Example Group that uses a shared block the "host group". Is there already a name for it? I never know what to use, and I'm not sure "host group" describes it accurately anyway.
>>
>> Cheers
>> Ash
>>
>> [1]http://github.com/rspec/rspec-core/issues/issue/99
>>
>> --http://www.patchspace.co.uk/http://www.linkedin.com/in/ashleymoran

> I may be the only one who finds this useful, but I think there's value
> in evaluating the customization block after the shared example group
> block. It allows the shared example group to provide a default
> implementation of a helper method, and then an instance of the shared
> behavior to override the helper method if appropriate. If you
> evaluate the customization block before the shared example group
> block, the default implementation wins out, and you have no way to
> override helper methods in an instance of a shared example group.

You can still get the same outcome, but you have to implement it in the group like this:

unless defined?(:foo)
def foo; "foo"; end
end

I think it's a good trade-off to put that burden on the group (and it's author) rather that the consumers of the group.

Ashley Moran

unread,
Jul 31, 2010, 3:56:03 AM7/31/10
to rspec-users

On 31 Jul 2010, at 1:10 AM, David Chelimsky wrote:

> You can still get the same outcome, but you have to implement it in the group like this:
>
> unless defined?(:foo)
> def foo; "foo"; end
> end

Maybe a DSL method while I'm working on it? Maybe:

default_helper(:foo) do
"foo"
end

WDYT?


> I think it's a good trade-off to put that burden on the group (and it's author) rather that the consumers of the group.

Agreed, it'd cause a lot of duplication of effort the other way round.

_______________________________________________

Myron Marston

unread,
Jul 31, 2010, 2:06:47 PM7/31/10
to rspec...@rubyforge.org
> You can still get the same outcome, but you have to implement it in the group like this:

> unless defined?(:foo)
> def foo; "foo"; end
> end

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).

> Maybe a DSL method while I'm working on it?  Maybe:
>
>   default_helper(:foo) do
>     "foo"
>   end
>
> WDYT?

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.)

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. 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

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.

Myron


On Jul 31, 12:56 am, Ashley Moran <ashley.mo...@patchspace.co.uk>
wrote:


> On 31 Jul 2010, at 1:10 AM, David Chelimsky wrote:
>
> > You can still get the same outcome, but you have to implement it in the group like this:
>
> > unless defined?(:foo)
> >  def foo; "foo"; end
> > end
>
> Maybe a DSL method while I'm working on it?  Maybe:
>
>   default_helper(:foo) do
>     "foo"
>   end
>
> WDYT?
>
> > I think it's a good trade-off to put that burden on the group (and it's author) rather that the consumers of the group.
>
> Agreed, it'd cause a lot of duplication of effort the other way round.
>

> --http://www.patchspace.co.uk/http://www.linkedin.com/in/ashleymoran
>
> _______________________________________________
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

Ashley Moran

unread,
Jul 31, 2010, 3:42:18 PM7/31/10
to rspec-users

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

_______________________________________________
rspec-users mailing list
rspec...@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

Ashley Moran

unread,
Aug 1, 2010, 5:36:20 AM8/1/10
to rspec-users

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

> I think this is a clunky way to essentially pass a parameter to the
> shared example group. Better would be something like this:
>
> it_should_behave_like "something" do
> providing :method_name, :foo
> end

After sleeping on this, I found an elegant solution (elegant in Ruby 1.9 anyway, I had to monkeypatch Ruby 1.8 to make it work) to the class method problem. (It was exactly what I said before.)

So I *think* all the technical problems for this feature are solved, and the question is just how the it should actually behave...

Ash

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

_______________________________________________
rspec-users mailing list
rspec...@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

David Chelimsky

unread,
Aug 1, 2010, 10:48:01 AM8/1/10
to rspec-users
On Aug 1, 2010, at 9:43 AM, David Chelimsky wrote:

>
> On Jul 31, 2010, at 1:06 PM, Myron Marston wrote:
>
>>> You can still get the same outcome, but you have to implement it in the group like this:
>>
>>> unless defined?(:foo)
>>> def foo; "foo"; end
>>> end
>>

>> 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.
>

> 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

> Agreed on both points: best of both worlds and confusing :)
>
> When I said "maybe a DSL" I was thinking only in the context of the shared group, not in the consuming group.
>
> What makes the example in your gist confusing to me is that we start to get into a different mental model of what a shared group is. Based on recent changes, for me, it's just a nested example group, which has well understood scoping rules. This introduces a new set of scoping rules that not only make this use case confusing, but it will lead to an expectation that this DSL be made available in other constructs.
>
> 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
>
> That wouldn't have worked with the old implementation, but it would work perfectly well now. This would also "just work" with hash-as-keyword-args:
>
> shared_examples_for "blah" do |options|
> it "blah #{options[:a]}" do
> ..
> end
> end
>
> it_should_behave_like "blah", :a => 1
>
> Now you can do this:
>
> [1,2,3].each do |n|
> it_should_behave_like "blah", :a => n
> end
>
> And we can still use the customization block to define methods, hooks (before/after) and let(). Now it just feels like the rest of RSpec.

Here's one way the measurement example could work in this format: http://gist.github.com/503432

>
> Thoughts?

David Chelimsky

unread,
Aug 1, 2010, 10:43:42 AM8/1/10
to rspec-users

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

>> You can still get the same outcome, but you have to implement it in the group like this:
>
>> unless defined?(:foo)
>> def foo; "foo"; end
>> end
>

> 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.

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

> So he/she

Agreed on both points: best of both worlds and confusing :)

When I said "maybe a DSL" I was thinking only in the context of the shared group, not in the consuming group.

What makes the example in your gist confusing to me is that we start to get into a different mental model of what a shared group is. Based on recent changes, for me, it's just a nested example group, which has well understood scoping rules. This introduces a new set of scoping rules that not only make this use case confusing, but it will lead to an expectation that this DSL be made available in other constructs.

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

That wouldn't have worked with the old implementation, but it would work perfectly well now. This would also "just work" with hash-as-keyword-args:

shared_examples_for "blah" do |options|
it "blah #{options[:a]}" do
..
end
end

it_should_behave_like "blah", :a => 1

Now you can do this:

[1,2,3].each do |n|
it_should_behave_like "blah", :a => n
end

And we can still use the customization block to define methods, hooks (before/after) and let(). Now it just feels like the rest of RSpec.

Thoughts?

Myron Marston

unread,
Aug 1, 2010, 12:40:19 PM8/1/10
to rspec...@rubyforge.org
> 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. 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.

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. I like eval'ing it last so that helper methods can be
overridden, _without_ anything special being done in the shared
example group.

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

David Chelimsky

unread,
Aug 1, 2010, 3:02:21 PM8/1/10
to rspec-users

<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

Ashley Moran

unread,
Aug 1, 2010, 6:12:40 PM8/1/10
to rspec-users

On 1 Aug 2010, at 3:43 PM, David Chelimsky wrote:

> shared_examples_for "blah" do |a,b|
> ...
> end
>
> it_should_behave_like "blah", 1, 2
>
> That wouldn't have worked with the old implementation, but it would work perfectly well now. This would also "just work" with hash-as-keyword-args:
>
> shared_examples_for "blah" do |options|
> it "blah #{options[:a]}" do
> ..
> end
> end
>
> it_should_behave_like "blah", :a => 1
>
> Now you can do this:
>
> [1,2,3].each do |n|
> it_should_behave_like "blah", :a => n
> end
>
> And we can still use the customization block to define methods, hooks (before/after) and let(). Now it just feels like the rest of RSpec.
>
> Thoughts?

One thought: me.facepalm :)

The only thing it lacks is a DSL to define the requirements. Would it still be desirable to be able to write:

shared_examples_for "blah" do |options|
require_argument options[:a]


it "blah #{options[:a]}" do
..
end
end

Or some such?

Also, after staring at this for a while, I'm puzzled by something. In this code:

it_should_behave_like "blah", :a => 1

how does :a => 1 get passed to the "options" block, as `shared_block` in the code is never called with arguments? Would this need another code change? (Apologies if I'm being thick, it's late and I should probably go to bed, but I wanted to review this first...)

Cheers
Ash

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

_______________________________________________
rspec-users mailing list
rspec...@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

Myron Marston

unread,
Aug 1, 2010, 6:39:02 PM8/1/10
to rspec...@rubyforge.org
> 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.

My mental model is indeed that the customization block is like a
subclass. I'm not sure where I got it--it's just the intuitive way I
understood shared_examples_for and it_should_behave_like. But if no
one else shares this mental model, then there's not much point in
making rspec work this way. I'm happy going with whatever the general
consensus is. Although, I do think that my mental model makes for
some interesting possibilities :).

> 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.

Maybe I misunderstood you here, but I took this to refer to the
passing of parameters to the shared example group, as you
suggested...and it turns out this isn't very hard at all:

http://github.com/myronmarston/rspec-core/commit/c353badcb8154ab98a7dc46eb19c8a9fc702ec73

The one issue with this is that it uses #module_exec, which is not
available in ruby 1.8.6--so we'd have to find a way to implement it,
similar to how cucumber implements #instance_exec when it's not
available:

http://github.com/aslakhellesoy/cucumber/blob/30d43767a7cffd1675e990115ac86c139e4ea3e0/lib/cucumber/core_ext/instance_exec.rb#L16-31

David Chelimsky

unread,
Aug 1, 2010, 7:00:27 PM8/1/10
to rspec-users
On Aug 1, 2010, at 5:39 PM, Myron Marston wrote:

>> 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.
>
> My mental model is indeed that the customization block is like a
> subclass. I'm not sure where I got it--it's just the intuitive way I
> understood shared_examples_for and it_should_behave_like. But if no
> one else shares this mental model, then there's not much point in
> making rspec work this way. I'm happy going with whatever the general
> consensus is. Although, I do think that my mental model makes for
> some interesting possibilities :).
>
>> 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.
>
> Maybe I misunderstood you here, but I took this to refer to the
> passing of parameters to the shared example group, as you
> suggested...and it turns out this isn't very hard at all:
>
> http://github.com/myronmarston/rspec-core/commit/c353badcb8154ab98a7dc46eb19c8a9fc702ec73

If we do this, we should use module_exec for both blocks so they both get the same arguments.

>
> The one issue with this is that it uses #module_exec, which is not
> available in ruby 1.8.6--so we'd have to find a way to implement it,
> similar to how cucumber implements #instance_exec when it's not
> available:
>
> http://github.com/aslakhellesoy/cucumber/blob/30d43767a7cffd1675e990115ac86c139e4ea3e0/lib/cucumber/core_ext/instance_exec.rb#L16-31

RSpec does that too :). Pretty sure it was Aslak that added that some years back.

Yeah - I'm playing around with an implementation of module_exec, but it doesn't seem to work quite the same way as instance exec does. Not yet, anyhow.

David

David Chelimsky

unread,
Aug 1, 2010, 6:52:31 PM8/1/10
to rspec-users

Actually, I just discovered that ruby 1.8.7 actually added module_exec. What does this mean? It means this:

def self.define_shared_group_method(new_name, report_label=nil)
module_eval(<<-END_RUBY, __FILE__, __LINE__)
def self.#{new_name}(name, *args, &customization_block)


shared_block = world.shared_example_groups[name]
raise "Could not find shared example group named \#{name.inspect}" unless shared_block

describe("#{report_label || "it should behave like"} \#{name}") do
module_exec *args, &shared_block
module_exec *args, &customization_block if customization_block
end
end
END_RUBY
end

What does that mean? It means this:

shared_examples_for "foo" do |a,b,c|
it "#{a} #{b} #{c}s" do
a.should do_something_with(b, c)
end
end

describe "something" do
it_behaves_like "foo", 1, 2, 3
end

Ta da!!!!!!

Two problems to solve at this point:

1. order of evaluation of blocks
2. what to do about ruby 1.8.6

re: order of evaluation of blocks, I think I'm inclined to go one way one minute, and another the next. Somebody convince me of one or the other.

re: 1.8.6, we've got a home-grown implementation of instance_exec that runs in 1.8.6 (although I just discovered that it's broken - fix coming shortly). I could

a) add such a thing for module_exec as well, though I haven't quite figured out how that works yet.
b) only support parameterized shared groups in ruby 1.8.7 or better
c). the most drastic option, would be to drop support for 1.8.6 entirely, but I don't think that's really feasible yet.

Thoughts?

David

> Cheers
> Ash

Myron Marston

unread,
Aug 1, 2010, 9:04:45 PM8/1/10
to rspec...@rubyforge.org
> If we do this, we should use module_exec for both blocks so they both get the same arguments.

I actually find the use of this to be a bit confusing:

[:foo, :bar].each do |arg|
it_should_behave_like "Something", arg do |a|
# The value of the param is already bound to arg and now it's
bound to a, too.
end
end

I suppose it may be useful in some situations, so I'm fine with it as
long as the implementation allows you to skip the `|a|`:

[:foo, :bar].each do |arg|
it_should_behave_like "Something", arg do
# no need to declare the |a| parameter since we already have it in
arg.
# I find this to be less confusing.
end
end

> Actually, I just discovered that ruby 1.8.7 actually added module_exec.

I should have mentioned that--my default ruby these days is 1.8.7, and
I found the same thing.

> re: order of evaluation of blocks, I think I'm inclined to go one way one minute, and another the next. Somebody convince me of one or the other.

Maybe it would be useful to make a list of the things that are
possible one way, but not the other, and vice versa...and then compare
these lists. Which list has the more useful and commonly needed use
cases?

> a) add such a thing for module_exec as well, though I haven't quite figured out how that works yet.

This is my vote. And I'm willing to take a stab at it--if for no
other reason then it'll help increase my understanding of ruby :).
I'm going to take a look at how rubinius implements this, and see what
ruby specs there are for it.

Myron

Myron Marston

unread,
Aug 1, 2010, 11:08:32 PM8/1/10
to rspec...@rubyforge.org
OK, I tried to implement #module_exec on ruby 1.8.6, and here's what I
came up with:

http://github.com/myronmarston/rspec-core/commit/364f20ebd5b7d9612227cb6e86a6e8c8c2e9931e

It works (at least in the sense that it allows the specs and features
I added in the previous commits in that branch to pass on ruby 1.8.6),
but I don't think it's correct. It just calls #instance_exec, but
instance_exec and module_exec are not the same (at least as I
understand them...). However, I based that implementation on rubinius
1.0.1's:

http://github.com/evanphx/rubinius/blob/release-1.0.1/kernel/common/module.rb#L438-441

Backports (a library that implements features of later versions of
ruby in 1.8.6) implements it in a similar fashion:

http://github.com/marcandre/backports/blob/v1.18.1/lib/backports/1.8.7/module.rb

Unfortunately, rubyspec doesn't provide much help in defining how
module_exec should work:

http://github.com/rubyspec/rubyspec/blob/master/core/module/module_exec_spec.rb

I'd need some concrete examples demonstrating how module_exec should
work (as compared to instance_exec) to implement it correctly. My
understanding is that they are identical except in regards to method
definitions--def's within an instance_exec define methods directly on
the Module object instance, whereas def's within a module_exec define
instance methods in the module (i.e. methods that will be added to any
class that includes the module).

One side issue I discovered is that instance_exec is implemented in
rspec-expectations, but not rspec-core (which, incidentally, is why I
linked to the cucumber implementation; I grepped in rspec-core and
couldn't find it). instance_exec is already used in rspec-core:

http://github.com/rspec/rspec-core/blob/v2.0.0.beta.19/lib/rspec/core/example_group.rb#L179

It needs to be implemented in rspec-core if you want rspec-core to be
able to be used on 1.8.6 without rspec-expectations.

Ashley Moran

unread,
Aug 2, 2010, 5:49:29 AM8/2/10
to rspec-users

On Aug 01, 2010, at 11:52 pm, David Chelimsky wrote:

> re: 1.8.6, we've got a home-grown implementation of instance_exec that runs in 1.8.6 (although I just discovered that it's broken - fix coming shortly). I could
>
> a) add such a thing for module_exec as well, though I haven't quite figured out how that works yet.
> b) only support parameterized shared groups in ruby 1.8.7 or better
> c). the most drastic option, would be to drop support for 1.8.6 entirely, but I don't think that's really feasible yet.

Hmmm. If you're working on a Rails project with RSpec 2 (which I'm not, but I'm guessing that will be a very common case), you need 1.8.7 anyway, as Rails 3 won't run on anything less. If you're not using Rails, I can't imagine anyone starting a new project on 1.8.6 now. (All my new stuff is on 1.9.2.)

Is 1.8.6 support in RSpec 2 *really* necessary? Any thoughts from anyone?

Cheers
Ash

_______________________________________________

David Chelimsky

unread,
Aug 2, 2010, 8:08:40 AM8/2/10
to rspec-users

On Aug 2, 2010, at 4:49 AM, Ashley Moran wrote:

>
> On Aug 01, 2010, at 11:52 pm, David Chelimsky wrote:
>
>> re: 1.8.6, we've got a home-grown implementation of instance_exec that runs in 1.8.6 (although I just discovered that it's broken - fix coming shortly). I could
>>
>> a) add such a thing for module_exec as well, though I haven't quite figured out how that works yet.
>> b) only support parameterized shared groups in ruby 1.8.7 or better
>> c). the most drastic option, would be to drop support for 1.8.6 entirely, but I don't think that's really feasible yet.
>
> Hmmm. If you're working on a Rails project with RSpec 2 (which I'm not, but I'm guessing that will be a very common case), you need 1.8.7 anyway, as Rails 3 won't run on anything less. If you're not using Rails, I can't imagine anyone starting a new project on 1.8.6 now. (All my new stuff is on 1.9.2.)

But what about people who are, for what ever reasons, stuck with Ruby 1.8.6 and want to upgrade? Also, there are a few rspec-2 + rails-2 efforts in the works, and there will be a solution for this sometime this fall.

We need to support 1.8.6.

Ashley Moran

unread,
Aug 2, 2010, 9:01:15 AM8/2/10
to rspec-users

On 2 Aug 2010, at 1:08 PM, David Chelimsky wrote:

> But what about people who are, for what ever reasons, stuck with Ruby 1.8.6 and want to upgrade? Also, there are a few rspec-2 + rails-2 efforts in the works, and there will be a solution for this sometime this fall.
>
> We need to support 1.8.6.

Ah fair enough. I didn't imagine there were many people stuck in that situation - I thought 1.8.6 was largely obsolete now. I also didn't know there were any RSpec-2/Rails-2 solutions in progress, I thought that was going to be unsupported.

David Chelimsky

unread,
Aug 2, 2010, 8:52:54 AM8/2/10
to rspec-users
On Aug 1, 2010, at 10:08 PM, Myron Marston wrote:

> OK, I tried to implement #module_exec on ruby 1.8.6, and here's what I
> came up with:
>
> http://github.com/myronmarston/rspec-core/commit/364f20ebd5b7d9612227cb6e86a6e8c8c2e9931e
>
> It works (at least in the sense that it allows the specs and features
> I added in the previous commits in that branch to pass on ruby 1.8.6),
> but I don't think it's correct.

If we're not exposing this as an API, and we're only using it in order to support this feature in RSpec when running Ruby 1.8.6, and it solves our problem, then I think it's correct as it needs to be.

Done: http://github.com/rspec/rspec-core/commit/7d492bdc3657ca9472368b50f3f3f6635aca7fe0

Ashley Moran

unread,
Aug 2, 2010, 9:16:33 AM8/2/10
to rspec-users

On 2 Aug 2010, at 2:04 AM, Myron Marston wrote:

> I actually find the use of this to be a bit confusing:
>
> [:foo, :bar].each do |arg|
> it_should_behave_like "Something", arg do |a|
> # The value of the param is already bound to arg and now it's
> bound to a, too.
> end
> end
>
> I suppose it may be useful in some situations, so I'm fine with it as
> long as the implementation allows you to skip the `|a|`:
>
> [:foo, :bar].each do |arg|
> it_should_behave_like "Something", arg do
> # no need to declare the |a| parameter since we already have it in
> arg.
> # I find this to be less confusing.
> end
> end


Agreed - requiring the block parameter to be declared in the host group is putting the onus back on the user, not the author, of the shared examples. I think we need a way to implement the second version.

Ash

_______________________________________________

Ashley Moran

unread,
Aug 2, 2010, 9:19:59 AM8/2/10
to rspec-users

On 2 Aug 2010, at 4:08 AM, Myron Marston wrote:

> Backports (a library that implements features of later versions of
> ruby in 1.8.6) implements it in a similar fashion:
>
> http://github.com/marcandre/backports/blob/v1.18.1/lib/backports/1.8.7/module.rb

Conceivably, RSpec 2 could depend on Backports under Ruby 1.8.6. It's not my opinion that it should (I don't have one), but I'm interested to know the implications.

Would that solve any design problems inside RSpec?

Would that cause any problems for users?

Would the problems solved outweigh the problems used?

Cheers
Ash

_______________________________________________

Ashley Moran

unread,
Aug 2, 2010, 9:23:20 AM8/2/10
to rspec-users

On 1 Aug 2010, at 11:52 PM, David Chelimsky wrote:

> re: order of evaluation of blocks, I think I'm inclined to go one way one minute, and another the next. Somebody convince me of one or the other.

One thing that may help clear this up is: can anyone offer a concrete example of where overriding a shared example group helper method would be useful (and better than an alternative design)?

My gut feeling is that, as David suggested, being able to override these is creating a class hierarchy of example groups. It feels to me unnervingly like overriding private methods. I wait to be proved wrong though, I just can't think of an example myself of wanting to do this.

Cheers
Ash

_______________________________________________

David Chelimsky

unread,
Aug 3, 2010, 7:22:28 AM8/3/10
to rspec-users
On Aug 2, 2010, at 7:52 AM, David Chelimsky wrote:

> On Aug 1, 2010, at 10:08 PM, Myron Marston wrote:
>
>> OK, I tried to implement #module_exec on ruby 1.8.6, and here's what I
>> came up with:
>>
>> http://github.com/myronmarston/rspec-core/commit/364f20ebd5b7d9612227cb6e86a6e8c8c2e9931e
>>
>> It works (at least in the sense that it allows the specs and features
>> I added in the previous commits in that branch to pass on ruby 1.8.6),
>> but I don't think it's correct.
>
> If we're not exposing this as an API, and we're only using it in order to support this feature in RSpec when running Ruby 1.8.6, and it solves our problem, then I think it's correct as it needs to be.

These are all true except for the "and it solves our problem" part. It almost does, but the one missing piece is that methods defined in the shared group are not available to its examples:

shared_examples_for "thing" do
def thing; Thing.new; do
it "does something" do
thing.should do_something
end
end

My inclination is to get this feature out with explicit non-support for 1.8.6, and then add support for 1.8.6 if we can get this to work. Working on that now - should be pushing some code (including Myron's contribution) later today.

Cheers,
David

Ashley Moran

unread,
Aug 3, 2010, 7:43:05 AM8/3/10
to rspec-users

On Aug 03, 2010, at 12:22 pm, David Chelimsky wrote:

> My inclination is to get this feature out with explicit non-support for 1.8.6, and then add support for 1.8.6 if we can get this to work. Working on that now - should be pushing some code (including Myron's contribution) later today.

Do you have everything in place to finish this off? Happy to help out if you want me to do any more coding on this, but it sounds like you've figured out a solution. In which case I'll sit back and see how the changes fit with the shared examples I've got so far...

Cheers
Ash

_______________________________________________

David Chelimsky

unread,
Aug 3, 2010, 7:50:39 AM8/3/10
to rspec-users
On Aug 3, 2010, at 6:43 AM, Ashley Moran wrote:

>
> On Aug 03, 2010, at 12:22 pm, David Chelimsky wrote:
>
>> My inclination is to get this feature out with explicit non-support for 1.8.6, and then add support for 1.8.6 if we can get this to work. Working on that now - should be pushing some code (including Myron's contribution) later today.
>
> Do you have everything in place to finish this off? Happy to help out if you want me to do any more coding on this, but it sounds like you've figured out a solution. In which case I'll sit back and see how the changes fit with the shared examples I've got so far...

Pushed:

http://github.com/rspec/rspec-core/commit/84303616be1ac2f8126675488947b47f6945cebe
http://github.com/rspec/rspec-core/commit/3cea7b8bea51766d632e20bcc9ef15c64b719ea1

Please do let me know if this works with what you've got.

The issue of the evaluation order is still up for grabs, but this now supports params to shared groups in Ruby >= 1.8.7.

Ashley Moran

unread,
Aug 3, 2010, 5:35:55 PM8/3/10
to rspec-users

On 3 Aug 2010, at 12:50 PM, David Chelimsky wrote:

Awesomeness!


> Please do let me know if this works with what you've got.

In general, yes, this is a massive improvement! I've realised some things that never occurred to me before, though. Maybe you have some thoughts...

I've put everything on a Gist[1] (which needs a few tweaks here and there, but I think it's a reasonably example). Notes:

* DomainLib is my holding module for everything I've extracted out of the project source. Anything inside that is generic, analogous to eg ActiveRecord (eg Entity <-> AR::Base)

* I've only pasted the specs, and only the contract-based ones at that (the implementation is not very interesting, nor is the interaction spec).

* I don't like the word contract any more, at least not here. It needs a better name, probably something that would fit if you wrote a similar spec for ActiveRecord's has_many.

Some things I ran into:

First, I found that you can't use the block variables in local helper methods. Because Ruby methods aren't closures, I've had to replace methods like:

def entity_dot_new_collection_member(*args)
entity.send(:"new_#{item_name}", *args)
end

with:

define_method :entity_dot_new_collection_member do |*args|
entity.send(:"new_#{item_name}", *args)
end

Not a big deal, but it's not as readable as it was before. (Not that it was exactly large-print Winnie the Pooh to start with, given the abstract nature of the shared examples.)

Second, you can't refer to `described_class` in the descriptions. I don't know why I though you'd be able to, but it would be nice if it worked :) (You can see the place where my failed attempt was, where I left <described_class>.)

Finally, I realised something when I added another example. I should say though, that all this time, I was only using the shared examples with one collection on the entity, and I added another a few minutes ago just for fun, and it just worked... I like :) But it raised a point about things that are common to all shared examples, and parameters to individual uses. In my example case, `entity_class` and `entity` are relevant to both of the "collection" shared example groups, but `collection_name`, `item_name`, `class_name` are parameters to the shared examples individually.

With the current setup, there's no way to require that a host group provides eg `entity_class`. And also, if it's defined as a `let` in the host, you can't use it in the descriptions in the shared example group (which you couldn't before, of course).

So I think this solves 90% of the problems I had before, and is certainly a workable solution to the specs I'm trying to write. I'd love to hear your thoughts on the rest though.


> The issue of the evaluation order is still up for grabs, but this now supports params to shared groups in Ruby >= 1.8.7.

Well, I deliberately didn't check what order you ended up using! Whatever it is works for me now, although I guess future experiments could change that...


Cheers!
Ash


[1] http://gist.github.com/507140

David Chelimsky

unread,
Aug 3, 2010, 8:05:02 PM8/3/10
to rspec-users

On Aug 3, 2010, at 4:35 PM, Ashley Moran wrote:

>
> On 3 Aug 2010, at 12:50 PM, David Chelimsky wrote:
>
>> Pushed:
>>
>> http://github.com/rspec/rspec-core/commit/84303616be1ac2f8126675488947b47f6945cebe
>> http://github.com/rspec/rspec-core/commit/3cea7b8bea51766d632e20bcc9ef15c64b719ea1
>
> Awesomeness!
>
>
>> Please do let me know if this works with what you've got.
>
> In general, yes, this is a massive improvement! I've realised some things that never occurred to me before, though. Maybe you have some thoughts...
>
> I've put everything on a Gist[1] (which needs a few tweaks here and there, but I think it's a reasonably example). Notes:
>
> * DomainLib is my holding module for everything I've extracted out of the project source. Anything inside that is generic, analogous to eg ActiveRecord (eg Entity <-> AR::Base)
>
> * I've only pasted the specs, and only the contract-based ones at that (the implementation is not very interesting, nor is the interaction spec).
>
> * I don't like the word contract any more, at least not here. It needs a better name, probably something that would fit if you wrote a similar spec for ActiveRecord's has_many.

I actually like contract a lot. Maybe we'll need alias_shared_examples_for_to :)

> Some things I ran into:
>
> First, I found that you can't use the block variables in local helper methods. Because Ruby methods aren't closures, I've had to replace methods like:
>
> def entity_dot_new_collection_member(*args)
> entity.send(:"new_#{item_name}", *args)
> end
>
> with:
>
> define_method :entity_dot_new_collection_member do |*args|
> entity.send(:"new_#{item_name}", *args)
> end
>
> Not a big deal, but it's not as readable as it was before. (Not that it was exactly large-print Winnie the Pooh to start with, given the abstract nature of the shared examples.)

This is just Ruby. It bugged me for a while too, but mostly because I kept forgetting. Now I'm completely accustomed to it and def and define_method seem quite the same to me.

> Second, you can't refer to `described_class` in the descriptions. I don't know why I though you'd be able to, but it would be nice if it worked :) (You can see the place where my failed attempt was, where I left <described_class>.)

This was a mis-alignment between names in the group and its examples (example_group.describes == example.described_class), but is now fixed (you can refer to described_class in both cases): http://github.com/rspec/rspec-core/commit/b236a8d8927da108097fed7982d1450e4701939d

> Finally, I realised something when I added another example. I should say though, that all this time, I was only using the shared examples with one collection on the entity, and I added another a few minutes ago just for fun, and it just worked... I like :) But it raised a point about things that are common to all shared examples, and parameters to individual uses. In my example case, `entity_class` and `entity` are relevant to both of the "collection" shared example groups, but `collection_name`, `item_name`, `class_name` are parameters to the shared examples individually.
>
> With the current setup, there's no way to require that a host group provides eg `entity_class`.

shared_examples_for "foo" do
raise "gotta define entity_class" unless public_instance_methods.map{|m|m.to_s).include?("entity_class")
end

> And also, if it's defined as a `let` in the host, you can't use it in the descriptions in the shared example group (which you couldn't before, of course).

Right - the only thing available to descriptions is going to be the params you pass in.

> So I think this solves 90% of the problems I had before, and is certainly a workable solution to the specs I'm trying to write. I'd love to hear your thoughts on the rest though.
>
>
>> The issue of the evaluation order is still up for grabs, but this now supports params to shared groups in Ruby >= 1.8.7.
>
> Well, I deliberately didn't check what order you ended up using! Whatever it is works for me now, although I guess future experiments could change that...

Thanks for all the feedback!

Cheers,
David

Myron Marston

unread,
Aug 4, 2010, 2:55:46 AM8/4/10
to rspec...@rubyforge.org
Ashley: thanks for posting the example. It's nice to see how this all
fits together.

Re: RSpec 2 for ruby 1.8.6: I don't see RSpec 2 as being all that
useful for Rails 2.x projects on ruby 1.8.6. However, it's still very
important for gems. I just converted one of my projects (VCR[1]) to
RSpec 2, and VCR supports ruby 1.8.6, 1.8.7 and 1.9.1. If we remove
ruby 1.8.6 support from RSpec 2, I'd have to migrate back to RSpec 1.x
so that I can continue to run the spec suite on 1.8.6. I imagine
there will be plenty of other libraries that will want to upgrade to
using RSpec 2 after the final release, while still supporting 1.8.6.

Good news: I messed around with module_exec some more, and I think I
have a working implementation for 1.8.6[2]. This was complicated
enough that I wanted to work on it in isolation from RSpec; hence the
separate github project. We'll probably want to re-organize it a bit
before merging it in, if it's deemed "good enough" to work for our
needs. It has some specs that pass for module_exec on 1.8.7, and they
pass on 1.8.6 with my implementation, too. There may be cases where
it still doesn't work quite right, though--feel free to fork, add
specs, etc.

Myron

[1] http://github.com/myronmarston/vcr
[2] http://github.com/myronmarston/module_exec

Ashley Moran

unread,
Aug 4, 2010, 4:22:47 AM8/4/10
to rspec-users

On 4 Aug 2010, at 1:05 AM, David Chelimsky wrote:

> I actually like contract a lot. Maybe we'll need alias_shared_examples_for_to :)

Haha, actually that gets +1 from me! Should I file a ticket? :)

In general I like contract, I just wasn't sure it was the right word for this usage of shared examples.

Maybe I just need to reword the shared examples, to write something like this:

it_satisfies_contract "container of", :children, :child, Child.name

(Obviously, if I had an inflection library in place, you could drop the last 2 args)


> This is just Ruby. It bugged me for a while too, but mostly because I kept forgetting. Now I'm completely accustomed to it and def and define_method seem quite the same to me.

Maybe. Perhaps then Ruby needs a neater closure-based method syntax eg:

foo = 1

defc my_method(bar)
foo + bar
end

or some such...


> This was a mis-alignment between names in the group and its examples (example_group.describes == example.described_class), but is now fixed (you can refer to described_class in both cases):http://github.com/rspec/rspec-core/commit/b236a8d8927da108097fed7982d1450e4701939d

Works for me! Ta :)


> shared_examples_for "foo" do
> raise "gotta define entity_class" unless public_instance_methods.map{|m|m.to_s).include?("entity_class")
> end

Aye, I guess I'm just in love with DSLs...

If I feel the need I might write a simple DSL and see if it's worth it.


>> And also, if it's defined as a `let` in the host, you can't use it in the descriptions in the shared example group (which you couldn't before, of course).
>
> Right - the only thing available to descriptions is going to be the params you pass in.

I have a feeling this will cause a misalignment, but maybe not. I'll work through some more practical examples and see how it plays out.


BTW any idea when the next beta will go out, so that this is in a released gem? I've got it working, but I had no luck using Bundler's :path option so I ended up having to build and install the gems into my project gemset. That's probably just a RubyGems/Bundler issue though.


Cheers
Ash

Ashley Moran

unread,
Aug 4, 2010, 4:30:41 AM8/4/10
to rspec-users

On 4 Aug 2010, at 7:55 AM, Myron Marston wrote:

> Ashley: thanks for posting the example. It's nice to see how this all
> fits together.

Arguably it would have made more sense to post that example *before*, rather than expecting you all to read my mind :)

I'm pleased with how it's working out so far. I need to write a lot more of these to know though. I only got as far as this one example before deciding to shave the shared example yak before moving on.


> Re: RSpec 2 for ruby 1.8.6: I don't see RSpec 2 as being all that
> useful for Rails 2.x projects on ruby 1.8.6. However, it's still very
> important for gems. I just converted one of my projects (VCR[1]) to
> RSpec 2, and VCR supports ruby 1.8.6, 1.8.7 and 1.9.1. If we remove
> ruby 1.8.6 support from RSpec 2, I'd have to migrate back to RSpec 1.x
> so that I can continue to run the spec suite on 1.8.6. I imagine
> there will be plenty of other libraries that will want to upgrade to
> using RSpec 2 after the final release, while still supporting 1.8.6.


Cool, if it's possible to maintain 1.8.6 support in RSpec 2 then by all means do so. I wasn't aware that such a large amount of code needed to run on 1.8.6, I assumed most had moved to 1.8.7. Doesn't look like it takes too much to monkeypatch 1.8.6 up to spec anyway.


Cheers
Ash

_______________________________________________

Ashley Moran

unread,
Aug 4, 2010, 4:43:24 AM8/4/10
to rspec-users

On 4 Aug 2010, at 1:05 AM, David Chelimsky wrote:

>


One other thought I've had is keyword syntax. While currently I'm writing:

it_satisfies_contract "[Entity] Collection:", :children, :child, Child.name

I prefer keyword arguments, so I'd like to write:

it_satisfies_contract "[Entity] Collection:",
:children,
item_name: "child",
class_name: Child.name

Currently that would mean rewriting the contract like this:

contract "[Entity] Collection:" do |collection_name, options|

# ...

describe "#{collection_name}" do
describe "Helper methods:" do
describe "#new_#{options[:item_name]}, #get_#{options[:item_name]}" do

# ...

WDYT about RSpec automatically translating keyword options to methods? They'd need to be defined as singleton class methods and instance methods to have the same availability as block parameters.

Ash

David Chelimsky

unread,
Aug 4, 2010, 7:41:03 AM8/4/10
to rspec-users

On Aug 4, 2010, at 3:43 AM, Ashley Moran wrote:

>
> On 4 Aug 2010, at 1:05 AM, David Chelimsky wrote:
>
>>
>
>
> One other thought I've had is keyword syntax. While currently I'm writing:
>
> it_satisfies_contract "[Entity] Collection:", :children, :child, Child.name
>
> I prefer keyword arguments, so I'd like to write:
>
> it_satisfies_contract "[Entity] Collection:",
> :children,
> item_name: "child",
> class_name: Child.name
>
> Currently that would mean rewriting the contract like this:
>
> contract "[Entity] Collection:" do |collection_name, options|
>
> # ...
>
> describe "#{collection_name}" do
> describe "Helper methods:" do
> describe "#new_#{options[:item_name]}, #get_#{options[:item_name]}" do
>
> # ...
>
> WDYT about RSpec automatically translating keyword options to methods?

What happens if the shared spec author really wants it to just be a hash? Do you think that's a valid use case?

David Chelimsky

unread,
Aug 4, 2010, 7:44:51 AM8/4/10
to rspec-users

On Aug 4, 2010, at 1:55 AM, Myron Marston wrote:

> Ashley: thanks for posting the example. It's nice to see how this all
> fits together.
>
> Re: RSpec 2 for ruby 1.8.6: I don't see RSpec 2 as being all that
> useful for Rails 2.x projects on ruby 1.8.6. However, it's still very
> important for gems. I just converted one of my projects (VCR[1]) to
> RSpec 2, and VCR supports ruby 1.8.6, 1.8.7 and 1.9.1. If we remove
> ruby 1.8.6 support from RSpec 2, I'd have to migrate back to RSpec 1.x
> so that I can continue to run the spec suite on 1.8.6. I imagine
> there will be plenty of other libraries that will want to upgrade to
> using RSpec 2 after the final release, while still supporting 1.8.6.
>
> Good news: I messed around with module_exec some more, and I think I
> have a working implementation for 1.8.6[2]. This was complicated
> enough that I wanted to work on it in isolation from RSpec; hence the
> separate github project. We'll probably want to re-organize it a bit
> before merging it in, if it's deemed "good enough" to work for our
> needs. It has some specs that pass for module_exec on 1.8.7, and they
> pass on 1.8.6 with my implementation, too. There may be cases where
> it still doesn't work quite right, though--feel free to fork, add
> specs, etc.

Hey Myron - I think what you have is perfectly fine. The only issue I ran into was that of defining instance methods, and your solution seems sound. I wouldn't even bother to undef those methods. We're not putting module_exec in as an API. In fact, in rspec, I think we should change the names of module_exec and instance_exec to something rspec-specific so that users don't rely on our implementation for other purposes. Something like:

def module_eval_with_args(*args, &block)
if respond_to?(:module_exec)
module_exec(*args, &block)
else
# custom solution
end
end

At that point, as long as all the shared group specs are passing, we're good. Make sense?

Myron Marston

unread,
Aug 4, 2010, 2:35:43 PM8/4/10
to rspec...@rubyforge.org
> I wouldn't even bother to undef those methods.

If we don't undef the methods, then the semantics of the
#module_eval_with_args (or whatever we call it) will be different on
1.8.6 and other versions. On 1.8.6, a method definition in the block
would define both an instance method _and_ a class method. Someone
could write a spec against 1.8.6, and accidentally call the class
method, not realizing they've done this, and the spec wouldn't work on
1.8.7 and above since the class method won't be there. So I think the
undefs are important, and I don't think it adds too much complexity.

> Something like:
>
> def module_eval_with_args(*args, &block)
>   if respond_to?(:module_exec)
>     module_exec(*args, &block)
>   else
>     # custom solution
>   end
> end
>
> At that point, as long as all the shared group specs are passing, we're good. Make sense?

Makes total sense. I'll work on porting this to RSpec, and open an
github issue with a link to the commits when I'm done.

Thanks!

David Chelimsky

unread,
Aug 4, 2010, 11:28:59 PM8/4/10
to rspec-users
On Aug 4, 2010, at 1:35 PM, Myron Marston wrote:

>> I wouldn't even bother to undef those methods.
>
> If we don't undef the methods, then the semantics of the
> #module_eval_with_args (or whatever we call it) will be different on
> 1.8.6 and other versions. On 1.8.6, a method definition in the block
> would define both an instance method _and_ a class method. Someone
> could write a spec against 1.8.6, and accidentally call the class
> method, not realizing they've done this, and the spec wouldn't work on
> 1.8.7 and above since the class method won't be there. So I think the
> undefs are important, and I don't think it adds too much complexity.
>
>> Something like:
>>
>> def module_eval_with_args(*args, &block)
>> if respond_to?(:module_exec)
>> module_exec(*args, &block)
>> else
>> # custom solution
>> end
>> end
>>
>> At that point, as long as all the shared group specs are passing, we're good. Make sense?
>
> Makes total sense. I'll work on porting this to RSpec, and open an
> github issue with a link to the commits when I'm done.

FYI - to those paying attention - I merged Myron's changes with support for parameterized shared groups even in 1.8.6.

At this point, the customization block is still being eval'd after the shared block, and I'm fairly well convinced this is the right thing, in combination with params to the block.

Next release will FINALLY have parameterized shared groups. Sweet!

Cheers,
David

Ashley Moran

unread,
Aug 6, 2010, 4:16:15 AM8/6/10
to rspec-users

On Aug 04, 2010, at 12:41 pm, David Chelimsky wrote:

> What happens if the shared spec author really wants it to just be a hash? Do you think that's a valid use case?

It could get in the way, then, I guess. You'd always have the original hash parameter if you wanted to use the method, but I guess it could cause trouble if you did this, or similar:

shared_examples_for "a foo container" do |foo, options = {}|
it "has a #{foo}" do; end
end

describe Bar do
it_should_behave_like "a foo container", 1, foo: 2
end

I'll probably play with this idea in my own code. There's definitely no need worry about it now, being able to pass arguments to shared example groups is 90% of the win for me.

Cheers

Ashley Moran

unread,
Aug 6, 2010, 4:18:03 AM8/6/10
to rspec-users

On Aug 05, 2010, at 4:28 am, David Chelimsky wrote:

> At this point, the customization block is still being eval'd after the shared block, and I'm fairly well convinced this is the right thing, in combination with params to the block.

I don't think it makes any different any more, at least not to me. The only thing you can't do is use class methods in shared example descriptions, but you don't need to do that any more now anyway.

> Next release will FINALLY have parameterized shared groups. Sweet!

Brilliant :-) What's the current release plan?

Cheers
Ash

_______________________________________________

David Chelimsky

unread,
Aug 6, 2010, 6:58:28 AM8/6/10
to rspec-users
On Aug 6, 2010, at 3:18 AM, Ashley Moran wrote:

>
> On Aug 05, 2010, at 4:28 am, David Chelimsky wrote:
>
>> At this point, the customization block is still being eval'd after the shared block, and I'm fairly well convinced this is the right thing, in combination with params to the block.
>
> I don't think it makes any different any more, at least not to me. The only thing you can't do is use class methods in shared example descriptions, but you don't need to do that any more now anyway.
>
>> Next release will FINALLY have parameterized shared groups. Sweet!
>
> Brilliant :-) What's the current release plan?

Barring the unforeseen, I'll knock out beta.20 this weekend.

David

>
> Cheers
> Ash

Ashley Moran

unread,
Aug 6, 2010, 7:00:42 AM8/6/10
to rspec-users

On Aug 06, 2010, at 11:58 am, David Chelimsky wrote:

> Barring the unforeseen, I'll knock out beta.20 this weekend.

Cool, ta!

Cheers
Ash

_______________________________________________

Reply all
Reply to author
Forward
0 new messages