Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Allow shared_examples to be defined per ExampleGroup. #818

Merged
merged 17 commits into from

5 participants

@JonRowe
Owner

This is an attempt at implementing #792. It allows shared_examples to be defined per ExampleGroup in order for keys to be safely reused.

It was more challengeing than it appeared due to the need to:

1) Inherit examples from outer context groups
2) Allow examples to be defined in main!

The issues I can see with this attemp are:

Extra methods being defined on main and thus Object.
Having to refactor the specs to make it pass, as the specs often defined things within the context of a test, but used them on a different context (an example group normally), the old global behaviour allowed this but the new context specific behaviour didn't.

Some ideas I could refactor towards:
Try to pull the examples into a hash lookup in Registry
Redefine how these are mixed into main so that examplegroups has different behaviour to main

There is probably stuff I've not thought of to do with RSpec's nested context class system thing...

Thoughts?

features/example_groups/shared_examples.feature
@@ -220,3 +220,39 @@ Feature: shared examples
"""
1 example, 0 failures
"""
+ Scenario: Shared examples are specific to their context
+ Given a file named "context_specific_examples_spec.rb" with:
+ """
+ describe do
+ context "my context" do
+ shared_examples "is independant" do
+ specify { expect(subject).to eq "context" }
+ end
+
+ subject { "context" }
@myronmarston Owner

The way you've demonstrated independence here (and in the context below) is a little convoluted, as it requires understanding the interplay between subject and the shared group. I think a clearer way to demonstrate it is to define different examples with different doc strings in the two shared example groups -- and then run the specs with --format doc and verify that the documentation output is different as expected.

Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
features/example_groups/shared_examples.feature
@@ -220,3 +220,39 @@ Feature: shared examples
"""
1 example, 0 failures
"""
+ Scenario: Shared examples are specific to their context
+ Given a file named "context_specific_examples_spec.rb" with:
+ """
+ describe do
+ context "my context" do
+ shared_examples "is independant" do
+ specify { expect(subject).to eq "context" }
+ end
+
+ subject { "context" }
+
+ it_should_behave_like "is independant"
@myronmarston Owner

FWIW, I prefer it_behaves_like "is independent" over it_should_behave_like...thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
features/example_groups/shared_examples.feature
((16 lines not shown))
+ end
+ context "another context" do
+ shared_examples "is independant" do
+ specify { expect(subject).to eq "another context" }
+ end
+
+ subject { "another context" }
+
+ it_should_behave_like "is independant"
+ end
+ context do
+ begin
+ it_should_behave_like "is independant"
+ fail "Shared examples should be dependant on context"
+ rescue ArgumentError
+ end
@myronmarston Owner

This is kinda hard to follow. (And I'm not even sure if fail is available in the example group context...but of course the point here is that it doesn't get to that line). Instead, what do you think about doing this?

  • Put this context in a separate file.
  • Have two When...Then sections for this scenario:
    • The first will run the file containing the two contexts that define and use an example groups
    • The second will run the file containing the context that (wrongly) uses the context even though one isn't available. It can verify that it fails with an appropriate error.
@JonRowe Owner
JonRowe added a note

FYI fail is a method of Kernel, it's available everywhere, fail 'message' is the same as doing raise 'message'

I'll look at separating this out into a separate scenario though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -29,7 +29,8 @@ module SharedExampleGroup
# @see ExampleGroup.include_examples
# @see ExampleGroup.include_context
def shared_examples *args, &block
- Registry.add_group(*args, &block)
+ context = self.is_a?(Class) ? self : self.class
@myronmarston Owner

I don't understand this ternary conditional -- can you explain it?

@JonRowe Owner
JonRowe added a note

If you define a shared example group in the main object (e.g. in support files or at the top of spec files) then self is an instance of object, otherwise self is a class definition (which is how the nested describe/context works in general). This is normalising the context to be a class.

@myronmarston Owner

That makes sense, but I think it would be easier to follow the logic if main.shared_examples was defined slightly differently from RSpec::Core::ExampleGroup#shared_examples to take care of that case. That way, it's explicit which case is needed from which context (since the logic for that case is explicitly defined for that context). Does that make sense?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -29,7 +29,8 @@ module SharedExampleGroup
# @see ExampleGroup.include_examples
# @see ExampleGroup.include_context
def shared_examples *args, &block
- Registry.add_group(*args, &block)
+ context = self.is_a?(Class) ? self : self.class
+ Registry.add_group(context,*args, &block)
@myronmarston Owner

As I usually comment -- spaces between args here, please :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -40,7 +41,15 @@ def shared_examples *args, &block
def share_as(name, &block)
RSpec.deprecate("Rspec::Core::SharedExampleGroup#share_as",
"RSpec::SharedContext or shared_examples")
- Registry.add_const(name, &block)
+ context = self.is_a?(Class) ? self : self.class
+ Registry.add_const(context,name, &block)
+ end
+
+ def shared_example_groups
+ ancestors[1..-1].inject(my_shared_example_groups) { |mine,other| mine.merge other.shared_example_groups }
+ end
+ def my_shared_example_groups
@myronmarston Owner

I like having blank lines between method definitions -- thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston
Owner

@JonRowe -- thanks for taking a stab at this (and sorry for taking so long to review it!). I left some comments on the diff itself but I'm realizing there's a deeper issue we need to discuss first: I believe this is a breaking change as it currently exists. There's a common pattern that goes like this:

# spec/support/shared_examples/something.rb
shared_examples_for "something" do
end
# spec/unit/my_class_spec.rb
require 'support/shared_examples/something'

describe MyClass do
  it_behaves_like "something"
end

Unless I'm reading the diff wrong, it looks like this pattern will no longer work. Here's what I think we want:

  • If a shared example group is defined at the top level, it is available anywhere.
  • If a shared example group is defined within an example group, it is only available in that example group and child groups.

Note, however, that even this behavior would be a breaking change from what we have now. Up to know, folks have been able to define a shared example group anywhere (including within a deeply nested example group), and use it from anywhere. So I think we'll have to hold off until 3.0 for this, and find a good way to print a deprecation for the existing behavior when it is used in this fashion in 2.99.

Thoughts?

@JonRowe
Owner

I rebased this and updated some of the styling, I'll work on a few of the other things soon.

@JonRowe
Owner

@myronmarston Apparently soon means now, I've rewritten the feature so it's better worded than before, does it make more sense now?

Top level examples are preserved here, basically at any point you can call shared_examples defined in your current context or any parent context including the top level (Object#main).

What is a breaking chance is calling shared examples cross context. I personally think that will be niche enough to deploy pre 3, but I'd accept waiting until 3. I have no idea how to nicely deprecate that scenario for 2.99 though... Basically have to branch off this code and add in the old cold so if this can't find the examples we warn and then use the old definition? Fugly but would work...

features/example_groups/shared_examples.feature
((29 lines not shown))
+ context do
+ shared_examples "shared examples are isolated" do
+ specify { expect(true).to eq true }
+ end
+ end
+
+ context do
+ it_behaves_like "shared examples are isolated"
+ end
+ end
+ """
+ When I run `rspec isolated_shared_examples_spec.rb`
+ Then the output should contain:
+ """
+ Could not find shared examples "shared examples are isolated"
+ """
@myronmarston Owner

This is much better -- thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff
spec/rspec/core/formatters/base_text_formatter_spec.rb
@@ -110,7 +110,7 @@ def run_all_and_dump_failures
context 'for #share_examples_for' do
it 'outputs the name and location' do
- share_examples_for 'foo bar' do
+ group.share_examples_for 'foo bar' do
@myronmarston Owner

Why the need to change from share_examples_for to group.share_examples_for here and elsewhere? It suggests that you've made this much more of a breaking change...

@JonRowe Owner
JonRowe added a note

This is down purely to not allowing not cross context scopes. The shared examples must therefore be declared and run from the same scope, which due to the way these spec's are written (they deliberately declare themselves outside of the current scope) looks a bit odd, because the spec's themselves are a bit odd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff
spec/rspec/core/world_spec.rb
((9 lines not shown))
world.reset
expect(world.example_groups).to be_empty
- expect(world.shared_example_groups).to be_empty
@myronmarston Owner

Seems like this should still clear the shared example groups defined at the top level, right? But not clear the ones defined within an example group.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston
Owner

What is a breaking chance is calling shared examples cross context. I personally think that will be niche enough to deploy pre 3, but I'd accept waiting until 3. I have no idea how to nicely deprecate that scenario for 2.99 though... Basically have to branch off this code and add in the old cold so if this can't find the examples we warn and then use the old definition? Fugly but would work...

After releasing 2.13 and seeing some of the things users do that we didn't expect at all (e.g. using a let or subject from within before(:all)) I'm a little paranoid about this. I think we can add the feature while retaining backwards compatibility if we do this:

  • When a shared group is defined within an example group, store it at the top-level scope as well, but do so with some state on it that indicates that it was defined from within an example group.
  • When a shared example group is used cross context, it'll look up the ancestors chain and find it at the top level scope.
  • When an example group is overridden we usually warn, but if it is being overridden at the top level, and was defined from within an example group (as indicated by the state I mentioned above), allow it to be overriden w/o a warning (since that would usually be OK, and we're only setting it at the top level for backwards compatibility).
  • In 2.99, we can actually use the extra bit of state indicating a top level shared group was defined an example group to issue a warning if/when it is ever used -- and then in 3.0 we can stop storing it at the top level scope.

Would that work, you think?

@JonRowe
Owner

Ok, well I've had some ideas' how this could be reworked, if we create a registry for shared examples, we can record where they were defined from without defining them on specific classes/instances. We can then set the rules of how they are allowed to be accessed and pass out deprecation messages accordingly. We could also potentially make this configurable, and just set a hardline default in 3.

@JonRowe
Owner

This warns about deprecation when accessing shared_examples across a context, but will default to the specific context first, it would be easy to remove the deprecation and cross context behaviour at a later date.

@JonRowe
Owner

Is this good to merge? /cc @myronmarston @soulcutter

@myronmarston

I know I still had some concerns before. I'll try to make time to review this tomorrow and make sure I don't have any more outstanding concerns. Thanks for pinging me and for your patience!

lib/rspec/core/shared_example_group.rb
@@ -71,7 +95,7 @@ def add_group(*args, &block)
end
end
- def add_const(name, &block)
+ def add_const(source,name, &block)
@myronmarston Owner

A space would be nice between args here :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -92,11 +116,23 @@ def self.included(kls)
end
shared_const = Object.const_set(name, mod)
- RSpec.world.shared_example_groups[shared_const] = block
+ add_shared_example_group source, shared_const, block
+ end
+
+ def shared_example_groups_for *sources
@myronmarston Owner

I know it's "seattle style" to not use parens in method defs, and I live in Seattle...but I really prefer the more common style of using parens in method defs (except in the case of no args). It's nice to have consistency within rspec's code base here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
((14 lines not shown))
end
private
+ def add_shared_example_group source, key, block
@myronmarston Owner

Ditto here...parens would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -105,8 +141,8 @@ def raise_name_error
raise NameError, "The first argument (#{name}) to share_as must be a legal name for a constant not already in use."
end
- def warn_if_key_taken key, new_block
- return unless existing_block = example_block_for(key)
+ def warn_if_key_taken source, key, new_block
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -121,8 +157,8 @@ def formatted_location block
block.source_location.join ":"
end
- def example_block_for key
- RSpec.world.shared_example_groups[key]
+ def example_block_for source, key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
@@ -0,0 +1,41 @@
+module RSpec
+ module Core
+ module SharedExampleGroup
+ class Collection
+
+ def initialize sources, examples
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((1 lines not shown))
+module RSpec
+ module Core
+ module SharedExampleGroup
+ class Collection
+
+ def initialize sources, examples
+ @sources, @examples = sources, examples
+ end
+
+ def [] key
+ fetch_examples(key) || warn_deprecation_and_fetch_anyway(key)
+ end
+
+ private
+
+ def fetch_examples key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((5 lines not shown))
+
+ def initialize sources, examples
+ @sources, @examples = sources, examples
+ end
+
+ def [] key
+ fetch_examples(key) || warn_deprecation_and_fetch_anyway(key)
+ end
+
+ private
+
+ def fetch_examples key
+ @examples[source_for key][key]
+ end
+
+ def source_for key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((9 lines not shown))
+
+ def [] key
+ fetch_examples(key) || warn_deprecation_and_fetch_anyway(key)
+ end
+
+ private
+
+ def fetch_examples key
+ @examples[source_for key][key]
+ end
+
+ def source_for key
+ @sources.reverse.find { |source| @examples[source].has_key? key }
+ end
+
+ def fetch_anyway key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((13 lines not shown))
+
+ private
+
+ def fetch_examples key
+ @examples[source_for key][key]
+ end
+
+ def source_for key
+ @sources.reverse.find { |source| @examples[source].has_key? key }
+ end
+
+ def fetch_anyway key
+ @examples.values.inject({},&:merge)[key]
+ end
+
+ def warn_deprecation_and_fetch_anyway key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
@@ -0,0 +1,41 @@
+module RSpec
+ module Core
+ module SharedExampleGroup
+ class Collection
+
+ def initialize sources, examples
+ @sources, @examples = sources, examples
+ end
+
+ def [] key
@myronmarston Owner

Ditto here...parents would be nice for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff
spec/rspec/core/shared_example_group/collection_spec.rb
((17 lines not shown))
+ #
+ let(:examples) do
+ Hash.new { |hash,k| hash[k] = Hash.new }.tap do |hash|
+ hash["main"] = { "top level group" => example_1 }
+ hash["nested 1"] = { "nested level one" => example_2 }
+ hash["nested 2"] = { "nested level two" => example_3 }
+ end
+ end
+ (1..3).each { |num| let("example_#{num}") { double "example #{num}" } }
+
+ context 'setup with one source, which is the top level' do
+
+ let(:collection) { Collection.new ['main'], examples }
+
+ it 'fetches examples from the top level' do
+ expect(collection['top level group']).to eq example_1
@myronmarston Owner

I think that to eq example_1 issues a warning (when run with warnings on) but to eq(example_1) doesn't. I'd like to get rspec warning free and keep it that way.

(I could be wrong about the warning; feel free to ignore this if so).

@JonRowe Owner
JonRowe added a note

Running VERBOSE=true be rspec spec/rspec/core/shared_example_group/collection_spec.rb results in no warnings at all on 1.9.3 or 2.0.0, Ruby should only raise a warning for a parentesis-less arguments like this if it's ambiguous, or at least that's my understanding...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((18 lines not shown))
+ end
+
+ def source_for key
+ @sources.reverse.find { |source| @examples[source].has_key? key }
+ end
+
+ def fetch_anyway key
+ @examples.values.inject({},&:merge)[key]
+ end
+
+ def warn_deprecation_and_fetch_anyway key
+ if (example = fetch_anyway key)
+ RSpec.warn_deprecation <<-WARNING.gsub(/^ +\|/, '')
+ Accessing shared_examples defined across contexts is deprecated.
+ Please declare shared_examples within a shared context, or at the top level
+ WARNING
@myronmarston Owner
  • I noticed you used the .gsub(/^ +\|/, '') pattern I've used elsewhere, but the point of that pattern is to be able to indent the heredoc text here based on the surrounding code, while not having it that indented when the warning is printed. If you're going to do the gsub thing you should put some leading pipes to delimit the left border of the warning paragraph.
  • A period at the end of the second sentence would be nice :).
  • I've found that it's really helpful if these sorts of warnings can include a "called from: ..." line indicating where the user triggered this. Without such a line, it's hard to track down the source of the warning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group.rb
@@ -105,8 +141,8 @@ def raise_name_error
raise NameError, "The first argument (#{name}) to share_as must be a legal name for a constant not already in use."
end
- def warn_if_key_taken key, new_block
- return unless existing_block = example_block_for(key)
+ def warn_if_key_taken source, key, new_block
+ return unless existing_block = example_block_for(source,key)
@myronmarston Owner

source, key would be nice :).

@myronmarston Owner

(Sometimes I feel bad about being anal about spacing...but I do think it helps readability!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/core/shared_example_group/collection.rb
((10 lines not shown))
+ def [] key
+ fetch_examples(key) || warn_deprecation_and_fetch_anyway(key)
+ end
+
+ private
+
+ def fetch_examples key
+ @examples[source_for key][key]
+ end
+
+ def source_for key
+ @sources.reverse.find { |source| @examples[source].has_key? key }
+ end
+
+ def fetch_anyway key
+ @examples.values.inject({},&:merge)[key]
@myronmarston Owner

Clever :).

Also, {}, &:merge would read a bit nicer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff
lib/rspec/core/world.rb
@@ -23,7 +22,6 @@ def initialize(configuration=RSpec.configuration)
def reset
example_groups.clear
- shared_example_groups.clear
@myronmarston Owner

Hmm...removing this clear seems like a potentially breaking change. Do you think it should still clear out the top-level shared groups?

@JonRowe Owner
JonRowe added a note

Restored

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston commented on the diff
lib/rspec/core/shared_example_group.rb
@@ -40,7 +40,31 @@ def shared_examples *args, &block
def share_as(name, &block)
RSpec.deprecate("Rspec::Core::SharedExampleGroup#share_as",
"RSpec::SharedContext or shared_examples")
- Registry.add_const(name, &block)
+ Registry.add_const(self, name, &block)
+ end
+
+ def shared_example_groups
+ Registry.shared_example_groups_for('main', *ancestors[0..-1])
@myronmarston Owner

I was hunting around trying to see if this handles getting a shared group from a parent context w/o a warning (since I think it should)....does this cause that to happen by passing the ancestors? I didn't see a spec specifically for that case -- would be good to have one since it's an important behavior, I think (or did I miss it?).

@JonRowe Owner
JonRowe added a note

I've added one into the feature file...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston

@JonRowe -- this is very well done. Nice work!

I did have some open questions above. I don't want to block this from being merged, though, so I'll trust your judgement on whether or not my questions above really need to be addressed or not. I'm just a drive by commenter on this one :).

@myronmarston

Oh yeah....a changelog entry would be nice as well :).

@JonRowe
Owner

@myronmarston I've made some changes as per your suggestions, when Travis comes back I'll merge this unless you object ;)

@samphippen samphippen commented on the diff
lib/rspec/core/shared_example_group/collection.rb
((23 lines not shown))
+
+ def fetch_anyway(key)
+ @examples.values.inject({}, &:merge)[key]
+ end
+
+ def warn_deprecation_and_fetch_anyway(key)
+ if (example = fetch_anyway key)
+ RSpec.warn_deprecation <<-WARNING.gsub(/^ /, '')
+ Accessing shared_examples defined across contexts is deprecated.
+ Please declare shared_examples within a shared context, or at the top level.
+ This message was generated at: #{caller(0)[5 ]}
+ WARNING
+ example
+ end
+ end
+
@samphippen Collaborator

@JonRowe there's a random line gap here, should we get rid of it? all the other ends are right next to each other in this file.

@JonRowe Owner
JonRowe added a note

It's not random, it's separating the class/module definition from the methods, It looks weird to me without it, especially as these are private methods and thus indented inwards a level... But I'll remove it if I'm overruled...

@samphippen Collaborator

I think it's totally fine if there's a reason. It just looked weird to me whilst scrolling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@JonRowe JonRowe merged commit 769e4d2 into rspec:master

1 check passed

Details default The Travis CI build passed
@phiggins

It doesn't look like this line is used and causes a warning when running rspec-core's tests. Is this an intentional addition?

Owner

I believe so... Removed...

@hosamaly

This implementation assumes that @shared_example_groups has already been initialized, whereas it might still be nil. I would suggest using the getter instead of directly accessing the instance variable, so the line should become like this:

def clear
  shared_example_groups.clear
end

This would ensure correct behavior regardless of state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
4 Changelog.md
@@ -38,6 +38,8 @@ Enhancements
* Configure ruby's warning behaviour with `--warnings` (Jon Rowe)
* Fix an obscure issue on old versions of `1.8.7` where `Time.dup` wouldn't
allow access to `Time.now` (Jon Rowe)
+* Make `shared_examples_for` context aware, so that keys may be safely reused
+ in multiple contexts without colliding. (Jon Rowe)
Bug fixes
@@ -70,6 +72,8 @@ Deprecations
* Deprecate `Configuration#requires=` in favor of using ruby's
`require`. Requires specified by the command line can still be
accessed by the `Configuration#require` reader. (Bradley Schaefer)
+* Deprecate calling `SharedExampleGroups` defined across sibling contexts
+ (Jon Rowe)
### 2.13.1 / 2013-03-12
[full changelog](http://github.com/rspec/rspec-core/compare/v2.13.0...v2.13.1)
View
72 features/example_groups/shared_examples.feature
@@ -220,3 +220,75 @@ Feature: shared examples
"""
1 example, 0 failures
"""
+
+ Scenario: Shared examples are nestable by context
+ Given a file named "context_specific_examples_spec.rb" with:
+ """Ruby
+ describe "shared examples" do
+ context "per context" do
+
+ shared_examples "shared examples are nestable" do
+ specify { expect(true).to eq true }
+ end
+
+ it_behaves_like "shared examples are nestable"
+ end
+ end
+ """
+ When I run `rspec context_specific_examples_spec.rb`
+ Then the output should contain:
+ """
+ 1 example, 0 failures
+ """
+
+ Scenario: Shared examples are accessible from offspring contexts
+ Given a file named "context_specific_examples_spec.rb" with:
+ """Ruby
+ describe "shared examples" do
+ shared_examples "shared examples are nestable" do
+ specify { expect(true).to eq true }
+ end
+
+ context "per context" do
+ it_behaves_like "shared examples are nestable"
+ end
+ end
+ """
+ When I run `rspec context_specific_examples_spec.rb`
+ Then the output should contain:
+ """
+ 1 example, 0 failures
+ """
+ And the output should not contain:
+ """
+ Accessing shared_examples defined across contexts is deprecated
+ """
+
+ Scenario: Shared examples are isolated per context
+ Given a file named "isolated_shared_examples_spec.rb" with:
+ """Ruby
+ describe "shared examples" do
+ context do
+ shared_examples "shared examples are isolated" do
+ specify { expect(true).to eq true }
+ end
+ end
+
+ context do
+ it_behaves_like "shared examples are isolated"
+ end
+ end
+ """
+ When I run `rspec isolated_shared_examples_spec.rb`
+ Then the output should contain:
+ """
+ 1 example, 0 failures
+ """
+ But the output should contain:
+ """
+ Accessing shared_examples defined across contexts is deprecated
+ """
+ And the output should contain:
+ """
+ isolated_shared_examples_spec.rb:9
+ """
View
1  lib/rspec/core.rb
@@ -36,6 +36,7 @@
require_rspec['core/command_line']
require_rspec['core/runner']
require_rspec['core/example']
+require_rspec['core/shared_example_group/collection']
require_rspec['core/shared_example_group']
require_rspec['core/example_group']
require_rspec['core/version']
View
2  lib/rspec/core/example_group.rb
@@ -162,7 +162,7 @@ def self.include_examples(name, *args, &block)
# @private
def self.find_and_eval_shared(label, name, *args, &customization_block)
raise ArgumentError, "Could not find shared #{label} #{name.inspect}" unless
- shared_block = world.shared_example_groups[name]
+ shared_block = shared_example_groups[name]
module_eval_with_args(*args, &shared_block)
module_eval(&customization_block) if customization_block
View
70 lib/rspec/core/shared_example_group.rb
@@ -28,8 +28,8 @@ module SharedExampleGroup
# @see ExampleGroup.it_behaves_like
# @see ExampleGroup.include_examples
# @see ExampleGroup.include_context
- def shared_examples *args, &block
- Registry.add_group(*args, &block)
+ def shared_examples(*args, &block)
+ Registry.add_group(self, *args, &block)
end
alias_method :shared_context, :shared_examples
@@ -40,7 +40,31 @@ def shared_examples *args, &block
def share_as(name, &block)
RSpec.deprecate("Rspec::Core::SharedExampleGroup#share_as",
"RSpec::SharedContext or shared_examples")
- Registry.add_const(name, &block)
+ Registry.add_const(self, name, &block)
+ end
+
+ def shared_example_groups
+ Registry.shared_example_groups_for('main', *ancestors[0..-1])
@myronmarston Owner

I was hunting around trying to see if this handles getting a shared group from a parent context w/o a warning (since I think it should)....does this cause that to happen by passing the ancestors? I didn't see a spec specifically for that case -- would be good to have one since it's an important behavior, I think (or did I miss it?).

@JonRowe Owner
JonRowe added a note

I've added one into the feature file...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+
+ module TopLevelDSL
+ def shared_examples(*args, &block)
+ Registry.add_group('main', *args, &block)
+ end
+
+ alias_method :shared_context, :shared_examples
+ alias_method :share_examples_for, :shared_examples
+ alias_method :shared_examples_for, :shared_examples
+
+ def share_as(name, &block)
+ RSpec.deprecate("Rspec::Core::SharedExampleGroup#share_as",
+ "RSpec::SharedContext or shared_examples")
+ Registry.add_const('main', name, &block)
+ end
+
+ def shared_example_groups
+ Registry.shared_example_groups_for('main')
+ end
end
# @private
@@ -53,13 +77,13 @@ def share_as(name, &block)
module Registry
extend self
- def add_group(*args, &block)
+ def add_group(source, *args, &block)
ensure_block_has_source_location(block, caller[1])
if key? args.first
key = args.shift
- warn_if_key_taken key, block
- RSpec.world.shared_example_groups[key] = block
+ warn_if_key_taken source, key, block
+ add_shared_example_group source, key, block
end
unless args.empty?
@@ -71,7 +95,7 @@ def add_group(*args, &block)
end
end
- def add_const(name, &block)
+ def add_const(source, name, &block)
if Object.const_defined?(name)
mod = Object.const_get(name)
raise_name_error unless mod.created_from_caller(caller)
@@ -92,12 +116,28 @@ def self.included(kls)
end
shared_const = Object.const_set(name, mod)
- RSpec.world.shared_example_groups[shared_const] = block
+ add_shared_example_group source, shared_const, block
+ end
+
+ def shared_example_groups_for(*sources)
+ Collection.new(sources, shared_example_groups)
+ end
+
+ def shared_example_groups
+ @shared_example_groups ||= Hash.new { |hash,key| hash[key] = Hash.new }
+ end
+
+ def clear
+ @shared_example_groups.clear
end
private
- def key? candidate
+ def add_shared_example_group(source, key, block)
+ shared_example_groups[source][key] = block
+ end
+
+ def key?(candidate)
[String, Symbol, Module].any? { |cls| cls === candidate }
end
@@ -105,8 +145,8 @@ def raise_name_error
raise NameError, "The first argument (#{name}) to share_as must be a legal name for a constant not already in use."
end
- def warn_if_key_taken key, new_block
- return unless existing_block = example_block_for(key)
+ def warn_if_key_taken(source, key, new_block)
+ return unless existing_block = example_block_for(source, key)
Kernel.warn <<-WARNING.gsub(/^ +\|/, '')
|WARNING: Shared example group '#{key}' has been previously defined at:
@@ -117,12 +157,12 @@ def warn_if_key_taken key, new_block
WARNING
end
- def formatted_location block
+ def formatted_location(block)
block.source_location.join ":"
end
- def example_block_for key
- RSpec.world.shared_example_groups[key]
+ def example_block_for(source, key)
+ shared_example_groups[source][key]
end
def ensure_block_has_source_location(block, caller_line)
@@ -139,6 +179,6 @@ def ensure_block_has_source_location(block, caller_line)
end
end
-extend RSpec::Core::SharedExampleGroup
+extend RSpec::Core::SharedExampleGroup::TopLevelDSL
Module.send(:include, RSpec::Core::SharedExampleGroup)
View
42 lib/rspec/core/shared_example_group/collection.rb
@@ -0,0 +1,42 @@
+module RSpec
+ module Core
+ module SharedExampleGroup
+ class Collection
+
+ def initialize(sources, examples)
+ @sources, @examples = sources, examples
+ end
+
+ def [](key)
+ fetch_examples(key) || warn_deprecation_and_fetch_anyway(key)
+ end
+
+ private
+
+ def fetch_examples(key)
+ @examples[source_for key][key]
+ end
+
+ def source_for(key)
+ @sources.reverse.find { |source| @examples[source].has_key? key }
+ end
+
+ def fetch_anyway(key)
+ @examples.values.inject({}, &:merge)[key]
+ end
+
+ def warn_deprecation_and_fetch_anyway(key)
+ if (example = fetch_anyway key)
+ RSpec.warn_deprecation <<-WARNING.gsub(/^ /, '')
+ Accessing shared_examples defined across contexts is deprecated.
+ Please declare shared_examples within a shared context, or at the top level.
+ This message was generated at: #{caller(0)[8]}
+ WARNING
+ example
+ end
+ end
+
@samphippen Collaborator

@JonRowe there's a random line gap here, should we get rid of it? all the other ends are right next to each other in this file.

@JonRowe Owner
JonRowe added a note

It's not random, it's separating the class/module definition from the methods, It looks weird to me without it, especially as these are private methods and thus indented inwards a level... But I'll remove it if I'm overruled...

@samphippen Collaborator

I think it's totally fine if there's a reason. It just looked weird to me whilst scrolling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+ end
+ end
+end
View
5 lib/rspec/core/world.rb
@@ -4,13 +4,12 @@ class World
include RSpec::Core::Hooks
- attr_reader :example_groups, :shared_example_groups, :filtered_examples
+ attr_reader :example_groups, :filtered_examples
attr_accessor :wants_to_quit
def initialize(configuration=RSpec.configuration)
@configuration = configuration
@example_groups = [].extend(Extensions::Ordered::ExampleGroups)
- @shared_example_groups = {}
@filtered_examples = Hash.new { |hash,group|
hash[group] = begin
examples = group.examples.dup
@@ -23,7 +22,7 @@ def initialize(configuration=RSpec.configuration)
def reset
example_groups.clear
- shared_example_groups.clear
@myronmarston Owner

Hmm...removing this clear seems like a potentially breaking change. Do you think it should still clear out the top-level shared groups?

@JonRowe Owner
JonRowe added a note

Restored

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ SharedExampleGroup::Registry.clear
end
def filter_manager
View
71 spec/rspec/core/example_group_spec.rb
@@ -995,20 +995,19 @@ def define_and_run_group(define_outer_example = false)
%w[include_examples include_context].each do |name|
describe "##{name}" do
+ let(:group) { ExampleGroup.describe }
before do
- shared_examples "named this" do
+ group.shared_examples "named this" do
example("does something") {}
end
end
it "includes the named examples" do
- group = ExampleGroup.describe
group.send(name, "named this")
expect(group.examples.first.description).to eq("does something")
end
it "raises a helpful error message when shared content is not found" do
- group = ExampleGroup.describe
expect do
group.send(name, "shared stuff")
end.to raise_error(ArgumentError, /Could not find .* "shared stuff"/)
@@ -1016,15 +1015,15 @@ def define_and_run_group(define_outer_example = false)
it "passes parameters to the shared content" do
passed_params = {}
+ group = ExampleGroup.describe
- shared_examples "named this with params" do |param1, param2|
+ group.shared_examples "named this with params" do |param1, param2|
it("has access to the given parameters") do
passed_params[:param1] = param1
passed_params[:param2] = param2
end
end
- group = ExampleGroup.describe
group.send(name, "named this with params", :value1, :value2)
group.run
@@ -1032,30 +1031,31 @@ def define_and_run_group(define_outer_example = false)
end
it "adds shared instance methods to the group" do
- shared_examples "named this with params" do |param1|
+ group = ExampleGroup.describe('fake group')
+ group.shared_examples "named this with params" do |param1|
def foo; end
end
- group = ExampleGroup.describe('fake group')
group.send(name, "named this with params", :a)
expect(group.public_instance_methods.map{|m| m.to_s}).to include("foo")
end
it "evals the shared example group only once" do
eval_count = 0
- shared_examples("named this with params") { |p| eval_count += 1 }
group = ExampleGroup.describe('fake group')
+ group.shared_examples("named this with params") { |p| eval_count += 1 }
group.send(name, "named this with params", :a)
expect(eval_count).to eq(1)
end
it "evals the block when given" do
key = "#{__FILE__}:#{__LINE__}"
- shared_examples(key) do
- it("does something") do
- expect(foo).to eq("bar")
- end
- end
group = ExampleGroup.describe do
+ shared_examples(key) do
+ it("does something") do
+ expect(foo).to eq("bar")
+ end
+ end
+
send name, key do
def foo; "bar"; end
end
@@ -1067,43 +1067,43 @@ def foo; "bar"; end
describe "#it_should_behave_like" do
it "creates a nested group" do
- shared_examples_for("thing") {}
group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") {}
group.it_should_behave_like("thing")
expect(group).to have(1).children
end
it "creates a nested group for a class" do
klass = Class.new
- shared_examples_for(klass) {}
group = ExampleGroup.describe('fake group')
+ group.shared_examples_for(klass) {}
group.it_should_behave_like(klass)
expect(group).to have(1).children
end
it "adds shared examples to nested group" do
- shared_examples_for("thing") do
+ group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") do
it("does something")
end
- group = ExampleGroup.describe('fake group')
shared_group = group.it_should_behave_like("thing")
expect(shared_group).to have(1).examples
end
it "adds shared instance methods to nested group" do
- shared_examples_for("thing") do
+ group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") do
def foo; end
end
- group = ExampleGroup.describe('fake group')
shared_group = group.it_should_behave_like("thing")
expect(shared_group.public_instance_methods.map{|m| m.to_s}).to include("foo")
end
it "adds shared class methods to nested group" do
- shared_examples_for("thing") do
+ group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") do
def self.foo; end
end
- group = ExampleGroup.describe('fake group')
shared_group = group.it_should_behave_like("thing")
expect(shared_group.methods.map{|m| m.to_s}).to include("foo")
end
@@ -1111,34 +1111,35 @@ def self.foo; end
it "passes parameters to the shared example group" do
passed_params = {}
- shared_examples_for("thing") do |param1, param2|
- it("has access to the given parameters") do
- passed_params[:param1] = param1
- passed_params[:param2] = param2
+ group = ExampleGroup.describe("group") do
+ shared_examples_for("thing") do |param1, param2|
+ it("has access to the given parameters") do
+ passed_params[:param1] = param1
+ passed_params[:param2] = param2
+ end
end
- end
- group = ExampleGroup.describe("group") do
it_should_behave_like "thing", :value1, :value2
end
+
group.run
expect(passed_params).to eq({ :param1 => :value1, :param2 => :value2 })
end
it "adds shared instance methods to nested group" do
- shared_examples_for("thing") do |param1|
+ group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") do |param1|
def foo; end
end
- group = ExampleGroup.describe('fake group')
shared_group = group.it_should_behave_like("thing", :a)
expect(shared_group.public_instance_methods.map{|m| m.to_s}).to include("foo")
end
it "evals the shared example group only once" do
eval_count = 0
- shared_examples_for("thing") { |p| eval_count += 1 }
group = ExampleGroup.describe('fake group')
+ group.shared_examples_for("thing") { |p| eval_count += 1 }
group.it_should_behave_like("thing", :a)
expect(eval_count).to eq(1)
end
@@ -1146,12 +1147,12 @@ def foo; end
context "given a block" do
it "evaluates the block in nested group" do
scopes = []
- shared_examples_for("thing") do
- it("gets run in the nested group") do
- scopes << self.class
- end
- end
group = ExampleGroup.describe("group") do
+ shared_examples_for("thing") do
+ it("gets run in the nested group") do
+ scopes << self.class
+ end
+ end
it_should_behave_like "thing" do
it("gets run in the same nested group") do
scopes << self.class
View
16 spec/rspec/core/formatters/base_text_formatter_spec.rb
@@ -110,7 +110,7 @@ def run_all_and_dump_failures
context 'for #share_examples_for' do
it 'outputs the name and location' do
- share_examples_for 'foo bar' do
+ group.share_examples_for 'foo bar' do
@myronmarston Owner

Why the need to change from share_examples_for to group.share_examples_for here and elsewhere? It suggests that you've made this much more of a breaking change...

@JonRowe Owner
JonRowe added a note

This is down purely to not allowing not cross context scopes. The shared examples must therefore be declared and run from the same scope, which due to the way these spec's are written (they deliberately declare themselves outside of the current scope) looks a bit odd, because the spec's themselves are a bit odd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
it("example name") { expect("this").to eq("that") }
end
@@ -127,7 +127,7 @@ def run_all_and_dump_failures
context 'that contains nested example groups' do
it 'outputs the name and location' do
- share_examples_for 'foo bar' do
+ group.share_examples_for 'foo bar' do
describe 'nested group' do
it("example name") { expect("this").to eq("that") }
end
@@ -151,7 +151,7 @@ def run_all_and_dump_failures
it 'outputs the name and location' do
- share_as :FooBar do
+ group.share_as :FooBar do
it("example name") { expect("this").to eq("that") }
end
@@ -169,7 +169,7 @@ def run_all_and_dump_failures
context 'that contains nested example groups' do
it 'outputs the name and location' do
- share_as :NestedFoo do
+ group.share_as :NestedFoo do
describe 'nested group' do
describe 'hell' do
it("example name") { expect("this").to eq("that") }
@@ -249,7 +249,7 @@ def run_all_and_dump_pending
context 'for #share_examples_for' do
it 'outputs the name and location' do
- share_examples_for 'foo bar' do
+ group.share_examples_for 'foo bar' do
it("example name") { pending { expect("this").to eq("that") } }
end
@@ -266,7 +266,7 @@ def run_all_and_dump_pending
context 'that contains nested example groups' do
it 'outputs the name and location' do
- share_examples_for 'foo bar' do
+ group.share_examples_for 'foo bar' do
describe 'nested group' do
it("example name") { pending { expect("this").to eq("that") } }
end
@@ -290,7 +290,7 @@ def run_all_and_dump_pending
it 'outputs the name and location' do
- share_as :FooBar2 do
+ group.share_as :FooBar2 do
it("example name") { pending { expect("this").to eq("that") } }
end
@@ -308,7 +308,7 @@ def run_all_and_dump_pending
context 'that contains nested example groups' do
it 'outputs the name and location' do
- share_as :NestedFoo2 do
+ group.share_as :NestedFoo2 do
describe 'nested group' do
describe 'hell' do
it("example name") { pending { expect("this").to eq("that") } }
View
70 spec/rspec/core/shared_example_group/collection_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+module RSpec::Core::SharedExampleGroup
+ describe Collection do
+
+ # this represents:
+ #
+ # shared_examples "top level group"
+ #
+ # context do
+ # shared_examples "nested level one"
+ # end
+ #
+ # context do
+ # shared_examples "nested level two"
+ # end
+ #
+ let(:examples) do
+ Hash.new { |hash,k| hash[k] = Hash.new }.tap do |hash|
+ hash["main"] = { "top level group" => example_1 }
+ hash["nested 1"] = { "nested level one" => example_2 }
+ hash["nested 2"] = { "nested level two" => example_3 }
+ end
+ end
+ (1..3).each { |num| let("example_#{num}") { double "example #{num}" } }
+
+ context 'setup with one source, which is the top level' do
+
+ let(:collection) { Collection.new ['main'], examples }
+
+ it 'fetches examples from the top level' do
+ expect(collection['top level group']).to eq example_1
@myronmarston Owner

I think that to eq example_1 issues a warning (when run with warnings on) but to eq(example_1) doesn't. I'd like to get rspec warning free and keep it that way.

(I could be wrong about the warning; feel free to ignore this if so).

@JonRowe Owner
JonRowe added a note

Running VERBOSE=true be rspec spec/rspec/core/shared_example_group/collection_spec.rb results in no warnings at all on 1.9.3 or 2.0.0, Ruby should only raise a warning for a parentesis-less arguments like this if it's ambiguous, or at least that's my understanding...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+
+ it 'fetches examples across the nested context' do
+ RSpec.stub(:warn_deprecation)
+ expect(collection['nested level two']).to eq example_3
+ end
+
+ it 'warns about deprecation when you fetch across nested contexts' do
+ RSpec.should_receive(:warn_deprecation)
+ collection['nested level two']
+ end
+ end
+
+ context 'setup with multiple sources' do
+
+ let(:collection) { Collection.new ['main','nested 1'], examples }
+
+ it 'fetches examples from the context' do
+ expect(collection['nested level one']).to eq example_2
+ end
+
+ it 'fetches examples from main' do
+ expect(collection['top level group']).to eq example_1
+ end
+
+ it 'fetches examples across the nested context' do
+ RSpec.stub(:warn_deprecation)
+ expect(collection['nested level two']).to eq example_3
+ end
+
+ it 'warns about deprecation when you fetch across nested contexts' do
+ RSpec.should_receive(:warn_deprecation)
+ collection['nested level two']
+ end
+
+ end
+ end
+end
View
6 spec/rspec/core/shared_example_group_spec.rb
@@ -33,10 +33,10 @@ module RSpec::Core
["name", :name, ExampleModule, ExampleClass].each do |object|
type = object.class.name.downcase
context "given a #{type}" do
- it "captures the given #{type} and block in the World's collection of shared example groups" do
+ it "captures the given #{type} and block in the collection of shared example groups" do
implementation = lambda {}
- RSpec.world.shared_example_groups.should_receive(:[]=).with(object, implementation)
send(shared_method_name, object, &implementation)
+ expect(SharedExampleGroup::Registry.shared_example_groups[self][object]).to eq implementation
end
end
end
@@ -55,8 +55,8 @@ module RSpec::Core
context "given a string and a hash" do
it "captures the given string and block in the World's collection of shared example groups" do
implementation = lambda {}
- RSpec.world.shared_example_groups.should_receive(:[]=).with("name", implementation)
send(shared_method_name, "name", :foo => :bar, &implementation)
+ expect(SharedExampleGroup::Registry.shared_example_groups[self]["name"]).to eq implementation
end
it "delegates extend on configuration" do
View
5 spec/rspec/core/world_spec.rb
@@ -10,12 +10,11 @@ module RSpec::Core
let(:world) { RSpec::Core::World.new(configuration) }
describe '#reset' do
- it 'clears #example_groups and #shared_example_groups' do
+ it 'clears #example_groups' do
world.example_groups << :example_group
- world.shared_example_groups[:shared] = :example_group
+ klass = Class.new
world.reset
expect(world.example_groups).to be_empty
- expect(world.shared_example_groups).to be_empty
@myronmarston Owner

Seems like this should still clear the shared example groups defined at the top level, right? But not clear the ones defined within an example group.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
end
end
Something went wrong with that request. Please try again.