Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Shared example group inclusion changes #2256

Merged
merged 8 commits into from
Jun 5, 2016
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Metrics/LineLength:

# This should go down over time.
Metrics/MethodLength:
Max: 40
Max: 37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woop :)


# This should go down over time.
Metrics/CyclomaticComplexity:
Expand Down
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ Enhancements:
others in a one-off manner. For example, `rspec spec/unit
spec/acceptance --order defined` will run unit specs before acceptance
specs. (Myron Marston, #2253)
* Add new `config.include_context` API for configuring global or
filtered inclusion of shared contexts in example groups.
(Myron Marston, #2256)
* Add new `config.shared_context_metadata_behavior = :apply_to_host_groups`
option, which causes shared context metadata to be inherited by the
metadata hash of all host groups and examples instead of configuring
implicit auto-inclusion based on the passed metadata. (Myron Marston, #2256)

Bug Fixes:

Expand Down
42 changes: 36 additions & 6 deletions features/example_groups/shared_context.feature
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
Feature: shared context

Use `shared_context` to define a block that will be evaluated in the context
of example groups either explicitly, using `include_context`, or implicitly by
matching metadata.
Use `shared_context` to define a block that will be evaluated in the context of example groups either locally, using `include_context` in an example group, or globally using `config.include_context`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the non broken up line for relish formatting? It's a real shame there isn't a better way...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. If we put line breaks, it'll render with line breaks on relish even if that's not a good place for it.__


When implicitly including shared contexts via matching metadata, the normal way is to define matching metadata on an example group, in which case the context is included in the entire group. However, you also have the option to include it in an individual example instead. RSpec treats every example as having a singleton example group (analogous to Ruby's singleton classes) containing just the one example.

Background:
Given a file named "shared_stuff.rb" with:
"""ruby
RSpec.shared_context "shared stuff", :a => :b do
RSpec.configure do |rspec|
# This config option will be enabled by default on RSpec 4,
# but for reasons of backwards compatibility, you have to
# set it on RSpec 3.
#
# It causes the host group and examples to inherit metadata
# from the shared context.
rspec.shared_context_metadata_behavior = :apply_to_host_groups
end
Copy link
Member

@JonRowe JonRowe Jun 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to go in Sam's blog post (something about this that is)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, planning on it :).


RSpec.shared_context "shared stuff", :shared_context => :metadata do
before { @some_var = :some_value }
def shared_method
"it works"
Expand All @@ -19,6 +27,10 @@ Feature: shared context
'this is the subject'
end
end

RSpec.configure do |rspec|
rspec.include_context "shared stuff", :include_shared => true
end
"""

Scenario: Declare a shared context and include it with `include_context`
Expand All @@ -44,6 +56,13 @@ Feature: shared context
it "accesses the subject defined in the shared context" do
expect(subject).to eq('this is the subject')
end

group = self

it "inherits metadata from the included context" do |ex|
expect(group.metadata).to include(:shared_context => :metadata)
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand Down Expand Up @@ -72,7 +91,7 @@ Feature: shared context
"""ruby
require "./shared_stuff.rb"

RSpec.describe "group that includes a shared context using metadata", :a => :b do
RSpec.describe "group that includes a shared context using metadata", :include_shared => true do
it "has access to methods defined in shared context" do
expect(shared_method).to eq("it works")
end
Expand All @@ -88,6 +107,13 @@ Feature: shared context
it "accesses the subject defined in the shared context" do
expect(subject).to eq('this is the subject')
end

group = self

it "inherits metadata from the included context" do |ex|
expect(group.metadata).to include(:shared_context => :metadata)
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand All @@ -103,9 +129,13 @@ Feature: shared context
expect(self).not_to respond_to(:shared_method)
end

it "has access to shared methods from examples with matching metadata", :a => :b do
it "has access to shared methods from examples with matching metadata", :include_shared => true do
expect(shared_method).to eq("it works")
end

it "inherits metadata form the included context due to the matching metadata", :include_shared => true do |ex|
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand Down
134 changes: 123 additions & 11 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,59 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
)
end

# @macro define_reader
# Configures how RSpec treats metadata passed as part of a shared example
# group definition. For example, given this shared example group definition:
#
# RSpec.shared_context "uses DB", :db => true do
# around(:example) do |ex|
# MyORM.transaction(:rollback => true, &ex)
# end
# end
#
# ...there are two ways RSpec can treat the `:db => true` metadata, each
# of which has a corresponding config option:
#
# 1. `:trigger_inclusion`: this shared context will be implicitly included
# in any groups (or examples) that have `:db => true` metadata.
# 2. `:apply_to_host_groups`: the metadata will be inherited by the metadata
# hash of all host groups and examples.
#
# `:trigger_inclusion` is the legacy behavior from before RSpec 3.5 but should
# be considered deprecated. Instead, you can explicitly include a group with
# `include_context`:
#
# RSpec.describe "My model" do
# include_context "uses DB"
# end
#
# ...or you can configure RSpec to include the context based on matching metadata
# using an API that mirrors configured module inclusion:
#
# RSpec.configure do |rspec|
# rspec.include_context "uses DB", :db => true
# end
#
# `:apply_to_host_groups` is a new feature of RSpec 3.5 and will be the only
# supported behavior in RSpec 4.
#
# @overload shared_context_metadata_behavior
# @return [:trigger_inclusion, :apply_to_host_groups] the configured behavior
# @overload shared_context_metadata_behavior=(value)
# @param value [:trigger_inclusion, :apply_to_host_groups] sets the configured behavior
define_reader :shared_context_metadata_behavior
# @see shared_context_metadata_behavior
def shared_context_metadata_behavior=(value)
case value
when :trigger_inclusion, :apply_to_host_groups
@shared_context_metadata_behavior = value
else
raise ArgumentError, "Cannot set `RSpec.configuration." \
"shared_context_metadata_behavior` to `#{value.inspect}`. Only " \
"`:trigger_inclusion` and `:apply_to_host_groups` are valid values."
end
end

# Record the start time of the spec suite to measure load time.
add_setting :start_time

Expand All @@ -352,6 +405,7 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
attr_reader :backtrace_formatter, :ordering_manager, :loaded_spec_files

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def initialize
# rubocop:disable Style/GlobalVars
@start_time = $_rspec_core_load_started_at || ::RSpec::Core::Time.now
Expand Down Expand Up @@ -398,9 +452,11 @@ def initialize
@threadsafe = true
@max_displayed_failure_line_count = 10
@world = World::Null
@shared_context_metadata_behavior = :trigger_inclusion

define_built_in_hooks
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize

# @private
Expand Down Expand Up @@ -1153,7 +1209,7 @@ def exclusion_filter
# end
#
# RSpec.configure do |config|
# config.include(UserHelpers) # included in all modules
# config.include(UserHelpers) # included in all groups
# config.include(AuthenticationHelpers, :type => :request)
# end
#
Expand All @@ -1172,12 +1228,55 @@ def exclusion_filter
# example has a singleton example group containing just the one
# example.
#
# @see #include_context
# @see #extend
# @see #prepend
def include(mod, *filters)
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
@include_modules.append(mod, meta)
on_existing_matching_groups(meta) { |group| safe_include(mod, group) }
define_mixed_in_module(mod, filters, @include_modules, :include) do |group|
safe_include(mod, group)
end
end

# Tells RSpec to include the named shared example group in example groups.
# Use `filters` to constrain the groups or examples in which to include
# the example group.
#
# @example
#
# RSpec.shared_context "example users" do
# let(:admin_user) { create_user(:admin) }
# let(:guest_user) { create_user(:guest) }
# end
#
# RSpec.configure do |config|
# config.include_context "example users", :type => :request
# end
#
# RSpec.describe "The admin page", :type => :request do
# it "can be viewed by admins" do
# login_with admin_user
# get "/admin"
# expect(response).to be_ok
# end
#
# it "cannot be viewed by guests" do
# login_with guest_user
# get "/admin"
# expect(response).to be_forbidden
# end
# end
#
# @note Filtered context inclusions can also be applied to
# individual examples that have matching metadata. Just like
# Ruby's object model is that every object has a singleton class
# which has only a single instance, RSpec's model is that every
# example has a singleton example group containing just the one
# example.
#
# @see #include
def include_context(shared_group_name, *filters)
shared_module = world.shared_example_group_registry.find([:main], shared_group_name)
include shared_module, *filters
end

# Tells RSpec to extend example groups with `mod`. Methods defined in
Expand Down Expand Up @@ -1211,9 +1310,9 @@ def include(mod, *filters)
# @see #include
# @see #prepend
def extend(mod, *filters)
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
@extend_modules.append(mod, meta)
on_existing_matching_groups(meta) { |group| safe_extend(mod, group) }
define_mixed_in_module(mod, filters, @extend_modules, :extend) do |group|
safe_extend(mod, group)
end
end

if RSpec::Support::RubyFeatures.module_prepends_supported?
Expand Down Expand Up @@ -1250,9 +1349,9 @@ def extend(mod, *filters)
# @see #include
# @see #extend
def prepend(mod, *filters)
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
@prepend_modules.append(mod, meta)
on_existing_matching_groups(meta) { |group| safe_prepend(mod, group) }
define_mixed_in_module(mod, filters, @prepend_modules, :prepend) do |group|
safe_prepend(mod, group)
end
end
end

Expand All @@ -1261,6 +1360,8 @@ def prepend(mod, *filters)
# Used internally to extend a group with modules using `include`, `prepend` and/or
# `extend`.
def configure_group(group)
group.hooks.register_globals(group, hooks)

configure_group_with group, @include_modules, :safe_include
configure_group_with group, @extend_modules, :safe_extend
configure_group_with group, @prepend_modules, :safe_prepend
Expand All @@ -1270,7 +1371,8 @@ def configure_group(group)
#
# Used internally to extend the singleton class of a single example's
# example group instance with modules using `include` and/or `extend`.
def configure_example(example)
def configure_example(example, example_hooks)
example_hooks.register_global_singleton_context_hooks(example, hooks)
singleton_group = example.example_group_instance.singleton_class

# We replace the metadata so that SharedExampleGroupModule#included
Expand Down Expand Up @@ -1956,6 +2058,16 @@ def safe_extend(mod, host)
end
# :nocov:
end

def define_mixed_in_module(mod, filters, mod_list, config_method, &block)
unless Module === mod
raise TypeError, "`RSpec.configuration.#{config_method}` expects a module but got: #{mod.inspect}"
end

meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
mod_list.append(mod, meta)
on_existing_matching_groups(meta, &block)
end
end
# rubocop:enable Metrics/ClassLength
end
Expand Down
10 changes: 8 additions & 2 deletions lib/rspec/core/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ def duplicate_with(metadata_overrides={})
new_metadata, new_metadata[:block])
end

# @private
def update_inherited_metadata(updates)
metadata.update(updates) do |_key, existing_example_value, _new_inherited_value|
existing_example_value
end
end

# @attr_reader
#
# Returns the first exception raised in the context of running this
Expand Down Expand Up @@ -229,8 +236,7 @@ def example_group
def run(example_group_instance, reporter)
@example_group_instance = example_group_instance
@reporter = reporter
hooks.register_global_singleton_context_hooks(self, RSpec.configuration.hooks)
RSpec.configuration.configure_example(self)
RSpec.configuration.configure_example(self, hooks)
RSpec.current_example = self

start(reporter)
Expand Down
24 changes: 17 additions & 7 deletions lib/rspec/core/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,16 @@ def self.remove_example(example)

# @private
def self.find_and_eval_shared(label, name, inclusion_location, *args, &customization_block)
shared_block = RSpec.world.shared_example_group_registry.find(parent_groups, name)
shared_module = RSpec.world.shared_example_group_registry.find(parent_groups, name)

unless shared_block
unless shared_module
raise ArgumentError, "Could not find shared #{label} #{name.inspect}"
end

SharedExampleGroupInclusionStackFrame.with_frame(name, Metadata.relative_path(inclusion_location)) do
module_exec(*args, &shared_block)
module_exec(&customization_block) if customization_block
end
shared_module.include_in(
self, Metadata.relative_path(inclusion_location),
args, customization_block
)
end

# @!endgroup
Expand Down Expand Up @@ -426,7 +426,6 @@ def self.set_it_up(description, args, registration_collection, &example_group_bl

@currently_executing_a_context_hook = false

hooks.register_globals(self, RSpec.configuration.hooks)
RSpec.configuration.configure_group(self)
end

Expand Down Expand Up @@ -690,6 +689,17 @@ class << self; self; end
# :nocov:
end

# @private
def self.update_inherited_metadata(updates)
metadata.update(updates) do |_key, existing_group_value, _new_inherited_value|
existing_group_value
end

RSpec.configuration.configure_group(self)
examples.each { |ex| ex.update_inherited_metadata(updates) }
children.each { |group| group.update_inherited_metadata(updates) }
end

# Raised when an RSpec API is called in the wrong scope, such as `before`
# being called from within an example rather than from within an example
# group block.
Expand Down
Loading