[rspec-users] Formatting shared example descriptions with test data

101 views
Skip to first unread message

Ashley Moran

unread,
Jul 26, 2010, 3:31:21 AM7/26/10
to rspec-users
Hi

I'm back again, and still on a quest to tame shared example to do my bidding. This time what I'm wondering is... is there any way to format shared example specdoc description output with data passed in with #let, or otherwise? eg: How do I get "1" and "2" into either of the placeholders <i> here?

shared_examples_for "Comparable" do
describe "comparing <i>" do
it "defines equality for <i>" do
object.should eq object
end
end
end

describe Integer do
it_should_behave_like "Comparable" do
let(:object) { 1 }
end
it_should_behave_like "Comparable" do
let(:object) { 2 }
end
end

(If I had to duplicate the 1 and 2 that wouldn't be the end of the world, as long as the syntax for including the shared examples was simple enough.)

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

Wincent Colaiuta

unread,
Jul 26, 2010, 3:55:55 AM7/26/10
to rspec-users
El 26/07/2010, a las 09:31, Ashley Moran escribió:

> I'm back again, and still on a quest to tame shared example to do my bidding. This time what I'm wondering is... is there any way to format shared example specdoc description output with data passed in with #let, or otherwise? eg: How do I get "1" and "2" into either of the placeholders <i> here?
>
> shared_examples_for "Comparable" do
> describe "comparing <i>" do
> it "defines equality for <i>" do
> object.should eq object
> end
> end
> end
>
> describe Integer do
> it_should_behave_like "Comparable" do
> let(:object) { 1 }
> end
> it_should_behave_like "Comparable" do
> let(:object) { 2 }
> end
> end
>
> (If I had to duplicate the 1 and 2 that wouldn't be the end of the world, as long as the syntax for including the shared examples was simple enough.)

Seems to me that including the same shared example group twice in the same "describe" block is a bit of an abuse, to be honest. I don't think it was ever really intended to be used in that way.

Your shared_examples_for should be written in such a way that they don't need to reference "<i>"; ie:

shared_examples_for "Comparable" do
it 'defines equality' do
subject.should eq(subject)
end
end

And then you use it like this:

describe Integer do
[1, 2].each do |i|
describe i do
it_should_behave_like 'Comparable'
end
end
end

Obviously this is a toy example that you've given us, and I don't see why you'd want to test 1 and 2 like that, but for the purposes of illustration... The specdoc output for that would look like:

Integer
1
it should behave like Comparable
defines equality
2
it should behave like Comparable
defines equality

I know you probably have some real example in mind hiding behind that toy example, but I believe anything you want to test can be written in the same way (ie. without needing to inject the "<i>" into your shared examples).

Cheers,
Wincent

Ashley Moran

unread,
Jul 26, 2010, 8:09:44 AM7/26/10
to rspec-users

On Jul 26, 2010, at 8:55 am, Wincent Colaiuta wrote:

> Seems to me that including the same shared example group twice in the same "describe" block is a bit of an abuse, to be honest. I don't think it was ever really intended to be used in that way.

You're right, it clearly wasn't intended for this. I'm trying to find the best way to express the behaviour I want without bending the current syntax of RSpec too much. This is indeed a toy example, so let me explain the real situation in more detail.

I'm doing a small side project to make a checklist app. As part of that, I'm trying to extract out a library similar to Naked Objects[1]. One of the things that can be factored out is collections inside entities. So I currently have, as examples:

class User
extend DomainLib::Entity::ClassExtensions
include DomainLib::Entity::InstanceExtensions

collection :checklist_templates, of: :checklist_template, class_name: "ChecklistTemplate"
end

and

class ChecklistTemplate
extend DomainLib::Entity::ClassExtensions
include DomainLib::Entity::InstanceExtensions

collection :items, of: :item, class_name: "ChecklistTemplateItem"
end

Now one of the thing that bugs me about using ORM (eg ActiveRecord, DataMapper) features for this is you're then faced with the dilemma of do you do an integration test of the collection functionality, which duplicates a lot of the testing effort put into the ORM, or do you mock this out, and risk having false positives because the ORM behaves differently than your test setup assumes?

The solution I'm playing with is to extract shared contract (ie shared example groups) that you can mix into a spec for a host class (eg User, Checklist) above to prove the feature (here collections) works, without reference to the implementation. (The specs inside DomainLib prove the general case.)

With the help of this spec_helper incantation:

module SpecHelperObjectExtensions
def contract(name, &block)
shared_examples_for(name, &block)
end
end
include SpecHelperObjectExtensions

RSpec.configure do |c|
c.alias_it_should_behave_like_to(:it_satisfies_contract, 'satisfies contract:')
end

I've already been able to extracted contract, which is for Representation (basically, a view object that isn't much more than a Struct):

# Params:
# * representation_class
# * properties
contract "Representation" do
# ...
# Setup and other examples omitted
# ...

describe "#==" do
it "is true for Representations with the equal attributes" do
representation_class.new(default_attributes).should eq representation_class.new(default_attributes)
end

it "is false if any property is different" do
properties.each do |property|
representation_class.new(default_attributes).should_not eq(
representation_class.new(default_attributes_with_different_value_for(property))
)
end
end
end

end

This is fine for a class, but the behaviour I want to prove with a Collection needs to be mixed in once per collection, eg (the last two are made up):

describe User do
it_satisfies_contract "Entity Collection", for: "checklist_templates"
it_satisfies_contract "Entity Collection", for: "groups"
it_satisfies_contract "Entity Collection", for: "delegated_actions"
end



> describe Integer do
> [1, 2].each do |i|
> describe i do
> it_should_behave_like 'Comparable'
> end
> end
> end
>

> ...


>
> I know you probably have some real example in mind hiding behind that toy example, but I believe anything you want to test can be written in the same way (ie. without needing to inject the "<i>" into your shared examples).

So as you can see, in the real (non-toy) example there's no object to be described: it's an aspect of behaviour of the test subject, and one than can occur N times. I'm aware that I'm twisting RSpec quite a bit to try to achieve this.

If you (or anyone) have any thoughts though, I'd love to hear them. This one is messing with my head a bit :)

Cheers
Ash


[1] http://www.nakedobjects.org/

_______________________________________________

Wincent Colaiuta

unread,
Jul 26, 2010, 10:44:26 AM7/26/10
to rspec-users
El 26/07/2010, a las 14:09, Ashley Moran escribió:

> The solution I'm playing with is to extract shared contract (ie shared example groups) that you can mix into a spec for a host class (eg User, Checklist) above to prove the feature (here collections) works, without reference to the implementation. (The specs inside DomainLib prove the general case.)
>
> With the help of this spec_helper incantation:
>
> module SpecHelperObjectExtensions
> def contract(name, &block)
> shared_examples_for(name, &block)
> end
> end
> include SpecHelperObjectExtensions

Personally I wouldn't do this. It makes it harder for anybody coming to your project to understand what's going on, because they see this "contract 'foo' do" construct and don't know what it is unless they dig into your spec_helper. If you really want the word contract to appear in there I would just write the shared examples like this:

shared_examples_for 'representation contract' do
...
end

> RSpec.configure do |c|
> c.alias_it_should_behave_like_to(:it_satisfies_contract, 'satisfies contract:')
> end

And if you go with names like "representation contract" then you might want your alias to be just "it_satisifies" instead...

> it "is false if any property is different" do
> properties.each do |property|
> representation_class.new(default_attributes).should_not eq(
> representation_class.new(default_attributes_with_different_value_for(property))
> )
> end
> end

So if I'm reading you correctly, this is where the "aspect of behavior that can occur N times" comes in, right? AFAIK the typical way to do this is via "macros" (ie. generating examples on the fly, typically keeping to examples of one assertion per iteration of the loop). So it's just a minor tweak of what you've got there:

properties.each do |property|
it "is false if #{property} is different" do
...
end
end

But alas, this pattern won't work with shared example groups. I don't know of any way to pass the "properties" variable in this case, and even if you could it wouldn't work anyway because, AFAIK, the shared example group is itself only evaluated once when the file is first read. It isn't re-evaluted each time it is included in another example group. At least, that's my understanding of it. I might be wrong about that. So, looks like you're stuck with having multiple assertions inside a single "it" block.

> This is fine for a class, but the behaviour I want to prove with a Collection needs to be mixed in once per collection, eg (the last two are made up):
>
> describe User do
> it_satisfies_contract "Entity Collection", for: "checklist_templates"
> it_satisfies_contract "Entity Collection", for: "groups"
> it_satisfies_contract "Entity Collection", for: "delegated_actions"
> end

Here the "standard" way of parametrizing this would be via blocks ("standard" in inverted commas because the ability to pass a block here is such a recent addition to RSpec).

Cheers,
Wincent

Ashley Moran

unread,
Jul 26, 2010, 12:15:47 PM7/26/10
to rspec-users

On Jul 26, 2010, at 3:44 pm, Wincent Colaiuta wrote:

> Personally I wouldn't do this. It makes it harder for anybody coming to your project to understand what's going on, because they see this "contract 'foo' do" construct and don't know what it is unless they dig into your spec_helper. If you really want the word contract to appear in there I would just write the shared examples like this:

Actually, this doesn't bother me. All the commercial development I do is pair-programmed, so it doesn't take long for anyone new to a project to pick things like these up. I prefer to improve the semantics at the expense of some initial learning cost. (As it happens, this isn't a commercial project anyway, it's just something I'm doing on the side for my own benefit.)


> So if I'm reading you correctly, this is where the "aspect of behavior that can occur N times" comes in, right? AFAIK the typical way to do this is via "macros" (ie. generating examples on the fly, typically keeping to examples of one assertion per iteration of the loop). So it's just a minor tweak of what you've got there:
>
> properties.each do |property|
> it "is false if #{property} is different" do
> ...
> end
> end

If you mean by using a custom matcher in there, then yeah, I get you. But as this is more extensive, shared examples (which prove many individual pieces of behaviour) seemed a better fit than a matcher (which are intended to prove just one).


> But alas, this pattern won't work with shared example groups. I don't know of any way to pass the "properties" variable in this case, and even if you could it wouldn't work anyway because, AFAIK, the shared example group is itself only evaluated once when the file is first read. It isn't re-evaluted each time it is included in another example group. At least, that's my understanding of it. I might be wrong about that. So, looks like you're stuck with having multiple assertions inside a single "it" block.

Hmmm, if that's the case, then this approach won't work anyway. But I'll investigate... it depends on the implementation. This is one of the things I want to find out.


>> This is fine for a class, but the behaviour I want to prove with a Collection needs to be mixed in once per collection, eg (the last two are made up):
>>
>> describe User do
>> it_satisfies_contract "Entity Collection", for: "checklist_templates"
>> it_satisfies_contract "Entity Collection", for: "groups"
>> it_satisfies_contract "Entity Collection", for: "delegated_actions"
>> end
>
> Here the "standard" way of parametrizing this would be via blocks ("standard" in inverted commas because the ability to pass a block here is such a recent addition to RSpec).

Yeah, I'm doing that to get the necessary objects inside the shared example. But as you pointed out, the scope inside example groups and inside the examples themselves are very different.


Cheers
Ash

_______________________________________________

Reply all
Reply to author
Forward
0 new messages