Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Alias example group #1236

Closed
wants to merge 5 commits into from

3 participants

@michihuber

allows adding custom aliases for example_group through the configuration

this is very much work in progress but I'll be offline for a bit, so maybe somebody can take a first look in the meantime
/cc @JonRowe @myronmarston

@myronmarston
Owner

Hey @michihuber -- the build is erroring because of some recent travis/bundler/rubygems changes that broke some things. Can you rebase against master? That should pull in .travis.yml changes that will fix the the install errors.

@myronmarston myronmarston commented on the diff
lib/rspec/core/dsl.rb
((21 lines not shown))
module Core
+ # DSL defines methods to group examples, most notably `describe`,
+ # and exposes them as class methods of {RSpec}.
+ # They can also be exposed globally (on main and Module) through
@myronmarston Owner

I think this line should be:

# They can be exposed globally (on `main` and instances of `Module`) through
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/dsl.rb
((51 lines not shown))
+
+ def self.expose_example_group_alias(name)
+ example_group_aliases << name
+
+ (class << RSpec; self; end).send(:define_method, name) do |*args, &example_group_block|
+ RSpec::Core::ExampleGroup.send(name, *args, &example_group_block).register
+ end
+ end
+
+ # Defines a named context for one or more examples
+ # @example_group
+ # RSpec.example_group do
+ # it "does something" do
+ # end
+ # end
+ expose_example_group_alias(:example_group)
@myronmarston Owner

This line is pretty odd...you are exposing example_group as an alias of example_group?

I'm a bit on the fence about whether or not it's a good thing to add example_group as a new method to the DSL. Thoughts from others on that?

@JonRowe Owner
JonRowe added a note

It's pretty odd, personally I like the idea of it being example group internally, then use this mechanism to expose the DSL as describe and context. I personally don't think we should add example_group to the top level DSL, but this should easily allow those that wish to expose it that way can do so.

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/dsl.rb
((46 lines not shown))
module DSL
+ # @private
+ def self.example_group_aliases
+ @example_group_aliases ||= []
+ end
+
+ def self.expose_example_group_alias(name)
+ example_group_aliases << name
+
+ (class << RSpec; self; end).send(:define_method, name) do |*args, &example_group_block|
+ RSpec::Core::ExampleGroup.send(name, *args, &example_group_block).register
@myronmarston Owner

Please use __send__ rather than send. While send works fine here, elsewhere we've had to use __send__ to deal with objects that have redefined send (e.g. Email#send), and it's good to be consistent and use __send__ everywhere -- that way we're less likely to forget to use it for places where it's important.

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/dsl.rb
@@ -35,26 +66,32 @@ def self.exposed_globally?
def self.expose_globally!
return if exposed_globally?
- to_define = proc do
- def describe(*args, &block)
- ::RSpec.describe(*args, &block)
+ example_group_aliases.each do |method_name|
+ to_define = proc do
+ send(:define_method, method_name) do |*args, &block|
+ ::RSpec.send(method_name, *args, &block)
@myronmarston Owner

Same here; __send__ rather than send, please.

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/dsl.rb
@@ -62,5 +99,5 @@ def self.remove_globally!
end
end
-# cature main without an eval
+# capture main without an eval
@myronmarston Owner

Thanks for correcting this typo :).

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/example_group.rb
@@ -124,13 +124,21 @@ def alias_example_to name, extra={}
(class << self; self; end).define_example_method name, extra
end
+ def alias_example_group_to(name, metadata={})
+ (class << self; self; end).send(:define_method, name) do |*args, &block|
@myronmarston Owner

Same here: __send__ please.

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/example_group.rb
@@ -124,13 +124,21 @@ def alias_example_to name, extra={}
(class << self; self; end).define_example_method name, extra
end
+ def alias_example_group_to(name, metadata={})
+ (class << self; self; end).send(:define_method, name) do |*args, &block|
+ combined_metadata = metadata.dup
+ combined_metadata.merge!(args.pop) if args.last.is_a? Hash
@myronmarston Owner

This does handle symbols-as-metadata (with true values) properly. Here's how we do it in alias_example_to:

options = Metadata.build_hash_from(args)
options.update(metadata)

I think it would make sense to do the same here. It'll need a spec as well, as I think it's important behavior.

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/example_group.rb
@@ -242,7 +250,8 @@ def self.describe(*args, &example_group_block)
end
class << self
- alias_method :context, :describe
+ alias_method :describe, :example_group
+ alias_method :context, :example_group
@myronmarston Owner

Now that we have a more general mechanism for declaring example group aliases, any reason not to leverage that here rather than just alias_method?

@myronmarston Owner

There are a number of other built-in aliases I would like to see (for parity with the example aliases):

  • xdescribe (shortcut for making an example group pending, like xit)
  • xcontext (same)
  • fdescribe (shortcut for focusing on an example group, like fit)
  • fcontext (same)

I'm not sure I want to add fexample_group and xexample_group, though.

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/dsl_spec.rb
((4 lines not shown))
+ shared_examples_for "a dsl method" do
+ it "is added to the main object and Module when expose_dsl_globally is enabled" do
+ in_sub_process do
+ RSpec.configuration.expose_dsl_globally = true
+ expect(main).to respond_to(method_name)
+ expect(Module.new).to respond_to(method_name)
+ end
+ end
+
+ it "is added to the RSpec DSL" do
+ expect(::RSpec).to respond_to(method_name)
+ end
+
+ it "is not added to every object in the system" do
+ expect(Object.new).not_to respond_to(method_name)
+ end
@myronmarston Owner

There's nothing here that specs the fact that main and Module do not get these methods added when expose_dsl_globally is false. Would be good to add that.

@JonRowe Owner
JonRowe added a note

Also there's nothing I've seen that shows that this new code is safe against the rspec loading issue.
E.g. when you specify the aliases after rspec has loaded, it looks like they won't be exposed off main, Module unless someone toggles the expose method manually.

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

@michihuber -- I'd love to get this in 3.0.0.beta2, which we'll be releasing soon. Are you able to get to get back to this in the next couple days? If not, that's fine; I'll plan to take it across the finish line so we can merge it. I just wanted to check first to make sure we don't step on each other's toes.

@myronmarston

I'm going to take a stab at getting this across the finish line. Thanks for getting this started, @michihuber!

@myronmarston myronmarston referenced this pull request
Merged

Alias example group #1255

@myronmarston

Closing in favor of #1255.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 22, 2013
  1. @michihuber

    first aliasing feature

    michihuber authored
  2. @michihuber
  3. @michihuber

    rename #describe to #example_group

    michihuber authored
    Conflicts:
    	lib/rspec/core/example_group.rb
  4. @michihuber

    use new metadata interface

    michihuber authored
Commits on Dec 23, 2013
  1. @michihuber
This page is out of date. Refresh to see the latest.
View
39 features/example_groups/aliasing.feature
@@ -0,0 +1,39 @@
+Feature: aliasing
+ `describe` and `context` are the default aliases for `example_group`.
+ You can define your own aliases for `example_group` and give those
+ custom aliases default meta data.
+
+ Scenario: custom example group aliases with metadata
+ Given a file named "nested_example_group_aliases_spec.rb" with:
+ """ruby
+ RSpec.configure do |c|
+ c.alias_example_group_to :detail, :detailed => true
+ end
+
+ RSpec.detail "a detail" do
+ it "can do some less important stuff" do
+ end
+ end
+
+ RSpec.describe "a thing" do
+ describe "in broad strokes" do
+ it "can do things" do
+ end
+ end
+
+ detail "something less important" do
+ it "can do an unimportant thing" do
+ end
+ end
+ end
+ """
+ When I run `rspec nested_example_group_aliases_spec.rb --tag detailed -fdoc`
+ Then the output should contain:
+ """
+ a detail
+ can do some less important stuff
+
+ a thing
+ something less important
+ """
+
View
6 lib/rspec/core/configuration.rb
@@ -663,6 +663,12 @@ def alias_example_to(new_name, *args)
RSpec::Core::ExampleGroup.alias_example_to(new_name, extra_options)
end
+ def alias_example_group_to(new_name, *args)
+ extra_options = Metadata.build_hash_from(args)
+ RSpec::Core::ExampleGroup.alias_example_group_to(new_name, extra_options)
+ RSpec::Core::DSL.expose_example_group_alias(new_name)
+ end
+
# Define an alias for it_should_behave_like that allows different
# language (like "it_has_behavior" or "it_behaves_like") to be
# employed when including shared examples.
View
95 lib/rspec/core/dsl.rb
@@ -1,25 +1,56 @@
module RSpec
-
- # Defines a named context for one or more examples. The given block
- # is evaluated in the context of a generated subclass of
- # {RSpec::Core::ExampleGroup}
- #
- # ## Examples:
- #
- # describe "something" do
- # it "does something" do
- # # example code goes here
- # end
- # end
- #
- # @see ExampleGroup
- # @see ExampleGroup.describe
- def self.describe(*args, &example_group_block)
- RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
- end
-
module Core
+ # DSL defines methods to group examples, most notably `describe`,
+ # and exposes them as class methods of {RSpec}.
+ # They can also be exposed globally (on main and Module) through
@myronmarston Owner

I think this line should be:

# They can be exposed globally (on `main` and instances of `Module`) through
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # the {Configuration} option `expose_dsl_globally`.
+ #
+ # By default the methods `describe`, `context` and `example_group`
+ # are exposed. These methods define a named context for one or
+ # more examples. The given block is evaluated in the context of
+ # a generated subclass of {RSpec::Core::ExampleGroup}
+ #
+ # ## Examples:
+ #
+ # RSpec.describe "something" do
+ # context "when something is a certain way" do
+ # it "does something" do
+ # # example code goes here
+ # end
+ # end
+ # end
+ #
+ # @see ExampleGroup
+ # @see ExampleGroup.example_group
+ #
+
module DSL
+ # @private
+ def self.example_group_aliases
+ @example_group_aliases ||= []
+ end
+
+ def self.expose_example_group_alias(name)
+ example_group_aliases << name
+
+ (class << RSpec; self; end).send(:define_method, name) do |*args, &example_group_block|
+ RSpec::Core::ExampleGroup.send(name, *args, &example_group_block).register
@myronmarston Owner

Please use __send__ rather than send. While send works fine here, elsewhere we've had to use __send__ to deal with objects that have redefined send (e.g. Email#send), and it's good to be consistent and use __send__ everywhere -- that way we're less likely to forget to use it for places where it's important.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+ end
+
+ # Defines a named context for one or more examples
+ # @example_group
+ # RSpec.example_group do
+ # it "does something" do
+ # end
+ # end
+ expose_example_group_alias(:example_group)
@myronmarston Owner

This line is pretty odd...you are exposing example_group as an alias of example_group?

I'm a bit on the fence about whether or not it's a good thing to add example_group as a new method to the DSL. Thoughts from others on that?

@JonRowe Owner
JonRowe added a note

It's pretty odd, personally I like the idea of it being example group internally, then use this mechanism to expose the DSL as describe and context. I personally don't think we should add example_group to the top level DSL, but this should easily allow those that wish to expose it that way can do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # Defines a named context for one or more examples
+ # @example_group
+ expose_example_group_alias(:describe)
+ # Defines a named context for one or more examples
+ # @example_group
+ expose_example_group_alias(:context)
class << self
# @private
@@ -35,26 +66,32 @@ def self.exposed_globally?
def self.expose_globally!
return if exposed_globally?
- to_define = proc do
- def describe(*args, &block)
- ::RSpec.describe(*args, &block)
+ example_group_aliases.each do |method_name|
+ to_define = proc do
+ send(:define_method, method_name) do |*args, &block|
+ ::RSpec.send(method_name, *args, &block)
@myronmarston Owner

Same here; __send__ rather than send, please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
end
+
+ (class << top_level; self; end).instance_eval(&to_define)
+ Module.class_exec(&to_define)
end
- top_level.instance_eval(&to_define)
- Module.class_exec(&to_define)
@exposed_globally = true
end
def self.remove_globally!
return unless exposed_globally?
- to_undefine = proc do
- undef describe
+ example_group_aliases.each do |method_name|
+ to_undefine = proc do
+ send(:undef_method, method_name) if method_defined?(method_name)
+ end
+
+ (class << top_level; self; end).instance_eval(&to_undefine)
+ Module.class_exec(&to_undefine)
end
- top_level.instance_eval(&to_undefine)
- Module.class_exec(&to_undefine)
@exposed_globally = false
end
@@ -62,5 +99,5 @@ def self.remove_globally!
end
end
-# cature main without an eval
+# capture main without an eval
@myronmarston Owner

Thanks for correcting this typo :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
::RSpec::Core::DSL.top_level = self
View
17 lib/rspec/core/example_group.rb
@@ -124,13 +124,21 @@ def alias_example_to name, extra={}
(class << self; self; end).define_example_method name, extra
end
+ def alias_example_group_to(name, metadata={})
+ (class << self; self; end).send(:define_method, name) do |*args, &block|
@myronmarston Owner

Same here: __send__ please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ combined_metadata = metadata.dup
+ combined_metadata.merge!(args.pop) if args.last.is_a? Hash
@myronmarston Owner

This does handle symbols-as-metadata (with true values) properly. Here's how we do it in alias_example_to:

options = Metadata.build_hash_from(args)
options.update(metadata)

I think it would make sense to do the same here. It'll need a spec as well, as I think it's important behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ example_group(*args, combined_metadata, &block)
+ end
+ end
+
# @private
# @macro [attach] define_nested_shared_group_method
#
# @see SharedExampleGroup
def self.define_nested_shared_group_method(new_name, report_label="it should behave like")
define_method(new_name) do |name, *args, &customization_block|
- group = describe("#{report_label} #{name}") do
+ group = example_group("#{report_label} #{name}") do
find_and_eval_shared("examples", name, *args, &customization_block)
end
group.metadata[:shared_group_name] = name
@@ -218,7 +226,7 @@ def self.superclass_metadata
#
# describe "something" do # << This describe method is defined in
# # << RSpec::Core::DSL, included in the
- # # << global namespace
+ # # << global namespace (optional)
# before do
# do_something_before
# end
@@ -232,7 +240,7 @@ def self.superclass_metadata
# end
#
# @see DSL#describe
- def self.describe(*args, &example_group_block)
+ def self.example_group(*args, &example_group_block)
args << {} unless args.last.is_a?(Hash)
args.last.update(:example_group_block => example_group_block)
@@ -242,7 +250,8 @@ def self.describe(*args, &example_group_block)
end
class << self
- alias_method :context, :describe
+ alias_method :describe, :example_group
+ alias_method :context, :example_group
@myronmarston Owner

Now that we have a more general mechanism for declaring example group aliases, any reason not to leverage that here rather than just alias_method?

@myronmarston Owner

There are a number of other built-in aliases I would like to see (for parity with the example aliases):

  • xdescribe (shortcut for making an example group pending, like xit)
  • xcontext (same)
  • fdescribe (shortcut for focusing on an example group, like fit)
  • fcontext (same)

I'm not sure I want to add fexample_group and xexample_group, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
end
# @private
View
32 spec/rspec/core/configuration_spec.rb
@@ -1281,6 +1281,38 @@ def host.foobar; end
end
end
+ describe "#alias_example_group_to" do
+ after do
+ RSpec::Core::DSL.example_group_aliases.delete(:my_group_method)
+
+ RSpec.module_eval do
+ class << self
+ undef :my_group_method if method_defined? :my_group_method
+ end
+ end
+
+ RSpec::Core::ExampleGroup.module_eval do
+ class << self
+ undef :my_group_method if method_defined? :my_group_method
+ end
+ end
+ end
+
+ it_behaves_like "metadata hash builder" do
+ def metadata_hash(*args)
+ config.alias_example_group_to :my_group_method, *args
+ group = ExampleGroup.my_group_method("a group")
+ group.metadata
+ end
+ end
+
+ it "allows adding additional metadata" do
+ config.alias_example_group_to :my_group_method, { some: "thing" }
+ group = ExampleGroup.my_group_method("a group", another: "thing")
+ expect(group.metadata).to include(some: "thing", another: "thing")
+ end
+ end
+
describe "#alias_example_to" do
it_behaves_like "metadata hash builder" do
after do
View
42 spec/rspec/core/dsl_spec.rb
@@ -6,8 +6,28 @@
RSpec.describe "The RSpec DSL" do
include InSubProcess
+ shared_examples_for "a dsl method" do
+ it "is added to the main object and Module when expose_dsl_globally is enabled" do
+ in_sub_process do
+ RSpec.configuration.expose_dsl_globally = true
+ expect(main).to respond_to(method_name)
+ expect(Module.new).to respond_to(method_name)
+ end
+ end
+
+ it "is added to the RSpec DSL" do
+ expect(::RSpec).to respond_to(method_name)
+ end
+
+ it "is not added to every object in the system" do
+ expect(Object.new).not_to respond_to(method_name)
+ end
@myronmarston Owner

There's nothing here that specs the fact that main and Module do not get these methods added when expose_dsl_globally is false. Would be good to add that.

@JonRowe Owner
JonRowe added a note

Also there's nothing I've seen that shows that this new code is safe against the rspec loading issue.
E.g. when you specify the aliases after rspec has loaded, it looks like they won't be exposed off main, Module unless someone toggles the expose method manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+
methods = [
+ :example_group,
:describe,
+ :context,
:share_examples_for,
:shared_examples_for,
:shared_examples,
@@ -16,20 +36,16 @@
methods.each do |method_name|
describe "##{method_name}" do
- it "is added to the main object and Module when expose_dsl_globally is enabled" do
- in_sub_process do
- RSpec.configuration.expose_dsl_globally = true
- expect(main).to respond_to(method_name)
- expect(Module.new).to respond_to(method_name)
- end
- end
- it "is added to the RSpec DSL" do
- expect(::RSpec).to respond_to(method_name)
- end
- it "is not added to every object in the system" do
- expect(Object.new).not_to respond_to(method_name)
- end
+ let(:method_name) { method_name }
+ it_behaves_like "a dsl method"
end
end
+
+ describe "a custom example_group alias" do
+ before(:all) { RSpec.configuration.alias_example_group_to(:detail) }
+
+ let(:method_name) { :detail }
+ it_behaves_like "a dsl method"
+ end
end
Something went wrong with that request. Please try again.