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

Commit 76c706e

Browse files
committed
Merge pull request #2256 from rspec/myron/fix-1790
Shared example group inclusion changes
2 parents cc869c8 + 3589ab5 commit 76c706e

File tree

14 files changed

+591
-67
lines changed

14 files changed

+591
-67
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Metrics/LineLength:
2424

2525
# This should go down over time.
2626
Metrics/MethodLength:
27-
Max: 40
27+
Max: 37
2828

2929
# This should go down over time.
3030
Metrics/CyclomaticComplexity:

Changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ Enhancements:
1313
others in a one-off manner. For example, `rspec spec/unit
1414
spec/acceptance --order defined` will run unit specs before acceptance
1515
specs. (Myron Marston, #2253)
16+
* Add new `config.include_context` API for configuring global or
17+
filtered inclusion of shared contexts in example groups.
18+
(Myron Marston, #2256)
19+
* Add new `config.shared_context_metadata_behavior = :apply_to_host_groups`
20+
option, which causes shared context metadata to be inherited by the
21+
metadata hash of all host groups and examples instead of configuring
22+
implicit auto-inclusion based on the passed metadata. (Myron Marston, #2256)
1623

1724
Bug Fixes:
1825

features/example_groups/shared_context.feature

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
Feature: shared context
22

3-
Use `shared_context` to define a block that will be evaluated in the context
4-
of example groups either explicitly, using `include_context`, or implicitly by
5-
matching metadata.
3+
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`.
64

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

97
Background:
108
Given a file named "shared_stuff.rb" with:
119
"""ruby
12-
RSpec.shared_context "shared stuff", :a => :b do
10+
RSpec.configure do |rspec|
11+
# This config option will be enabled by default on RSpec 4,
12+
# but for reasons of backwards compatibility, you have to
13+
# set it on RSpec 3.
14+
#
15+
# It causes the host group and examples to inherit metadata
16+
# from the shared context.
17+
rspec.shared_context_metadata_behavior = :apply_to_host_groups
18+
end
19+
20+
RSpec.shared_context "shared stuff", :shared_context => :metadata do
1321
before { @some_var = :some_value }
1422
def shared_method
1523
"it works"
@@ -19,6 +27,10 @@ Feature: shared context
1927
'this is the subject'
2028
end
2129
end
30+
31+
RSpec.configure do |rspec|
32+
rspec.include_context "shared stuff", :include_shared => true
33+
end
2234
"""
2335

2436
Scenario: Declare a shared context and include it with `include_context`
@@ -44,6 +56,13 @@ Feature: shared context
4456
it "accesses the subject defined in the shared context" do
4557
expect(subject).to eq('this is the subject')
4658
end
59+
60+
group = self
61+
62+
it "inherits metadata from the included context" do |ex|
63+
expect(group.metadata).to include(:shared_context => :metadata)
64+
expect(ex.metadata).to include(:shared_context => :metadata)
65+
end
4766
end
4867
"""
4968
When I run `rspec shared_context_example.rb`
@@ -72,7 +91,7 @@ Feature: shared context
7291
"""ruby
7392
require "./shared_stuff.rb"
7493
75-
RSpec.describe "group that includes a shared context using metadata", :a => :b do
94+
RSpec.describe "group that includes a shared context using metadata", :include_shared => true do
7695
it "has access to methods defined in shared context" do
7796
expect(shared_method).to eq("it works")
7897
end
@@ -88,6 +107,13 @@ Feature: shared context
88107
it "accesses the subject defined in the shared context" do
89108
expect(subject).to eq('this is the subject')
90109
end
110+
111+
group = self
112+
113+
it "inherits metadata from the included context" do |ex|
114+
expect(group.metadata).to include(:shared_context => :metadata)
115+
expect(ex.metadata).to include(:shared_context => :metadata)
116+
end
91117
end
92118
"""
93119
When I run `rspec shared_context_example.rb`
@@ -103,9 +129,13 @@ Feature: shared context
103129
expect(self).not_to respond_to(:shared_method)
104130
end
105131
106-
it "has access to shared methods from examples with matching metadata", :a => :b do
132+
it "has access to shared methods from examples with matching metadata", :include_shared => true do
107133
expect(shared_method).to eq("it works")
108134
end
135+
136+
it "inherits metadata form the included context due to the matching metadata", :include_shared => true do |ex|
137+
expect(ex.metadata).to include(:shared_context => :metadata)
138+
end
109139
end
110140
"""
111141
When I run `rspec shared_context_example.rb`

lib/rspec/core/configuration.rb

Lines changed: 123 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,59 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
327327
)
328328
end
329329

330+
# @macro define_reader
331+
# Configures how RSpec treats metadata passed as part of a shared example
332+
# group definition. For example, given this shared example group definition:
333+
#
334+
# RSpec.shared_context "uses DB", :db => true do
335+
# around(:example) do |ex|
336+
# MyORM.transaction(:rollback => true, &ex)
337+
# end
338+
# end
339+
#
340+
# ...there are two ways RSpec can treat the `:db => true` metadata, each
341+
# of which has a corresponding config option:
342+
#
343+
# 1. `:trigger_inclusion`: this shared context will be implicitly included
344+
# in any groups (or examples) that have `:db => true` metadata.
345+
# 2. `:apply_to_host_groups`: the metadata will be inherited by the metadata
346+
# hash of all host groups and examples.
347+
#
348+
# `:trigger_inclusion` is the legacy behavior from before RSpec 3.5 but should
349+
# be considered deprecated. Instead, you can explicitly include a group with
350+
# `include_context`:
351+
#
352+
# RSpec.describe "My model" do
353+
# include_context "uses DB"
354+
# end
355+
#
356+
# ...or you can configure RSpec to include the context based on matching metadata
357+
# using an API that mirrors configured module inclusion:
358+
#
359+
# RSpec.configure do |rspec|
360+
# rspec.include_context "uses DB", :db => true
361+
# end
362+
#
363+
# `:apply_to_host_groups` is a new feature of RSpec 3.5 and will be the only
364+
# supported behavior in RSpec 4.
365+
#
366+
# @overload shared_context_metadata_behavior
367+
# @return [:trigger_inclusion, :apply_to_host_groups] the configured behavior
368+
# @overload shared_context_metadata_behavior=(value)
369+
# @param value [:trigger_inclusion, :apply_to_host_groups] sets the configured behavior
370+
define_reader :shared_context_metadata_behavior
371+
# @see shared_context_metadata_behavior
372+
def shared_context_metadata_behavior=(value)
373+
case value
374+
when :trigger_inclusion, :apply_to_host_groups
375+
@shared_context_metadata_behavior = value
376+
else
377+
raise ArgumentError, "Cannot set `RSpec.configuration." \
378+
"shared_context_metadata_behavior` to `#{value.inspect}`. Only " \
379+
"`:trigger_inclusion` and `:apply_to_host_groups` are valid values."
380+
end
381+
end
382+
330383
# Record the start time of the spec suite to measure load time.
331384
add_setting :start_time
332385

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

354407
# rubocop:disable Metrics/AbcSize
408+
# rubocop:disable Metrics/MethodLength
355409
def initialize
356410
# rubocop:disable Style/GlobalVars
357411
@start_time = $_rspec_core_load_started_at || ::RSpec::Core::Time.now
@@ -398,9 +452,11 @@ def initialize
398452
@threadsafe = true
399453
@max_displayed_failure_line_count = 10
400454
@world = World::Null
455+
@shared_context_metadata_behavior = :trigger_inclusion
401456

402457
define_built_in_hooks
403458
end
459+
# rubocop:enable Metrics/MethodLength
404460
# rubocop:enable Metrics/AbcSize
405461

406462
# @private
@@ -1153,7 +1209,7 @@ def exclusion_filter
11531209
# end
11541210
#
11551211
# RSpec.configure do |config|
1156-
# config.include(UserHelpers) # included in all modules
1212+
# config.include(UserHelpers) # included in all groups
11571213
# config.include(AuthenticationHelpers, :type => :request)
11581214
# end
11591215
#
@@ -1172,12 +1228,55 @@ def exclusion_filter
11721228
# example has a singleton example group containing just the one
11731229
# example.
11741230
#
1231+
# @see #include_context
11751232
# @see #extend
11761233
# @see #prepend
11771234
def include(mod, *filters)
1178-
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1179-
@include_modules.append(mod, meta)
1180-
on_existing_matching_groups(meta) { |group| safe_include(mod, group) }
1235+
define_mixed_in_module(mod, filters, @include_modules, :include) do |group|
1236+
safe_include(mod, group)
1237+
end
1238+
end
1239+
1240+
# Tells RSpec to include the named shared example group in example groups.
1241+
# Use `filters` to constrain the groups or examples in which to include
1242+
# the example group.
1243+
#
1244+
# @example
1245+
#
1246+
# RSpec.shared_context "example users" do
1247+
# let(:admin_user) { create_user(:admin) }
1248+
# let(:guest_user) { create_user(:guest) }
1249+
# end
1250+
#
1251+
# RSpec.configure do |config|
1252+
# config.include_context "example users", :type => :request
1253+
# end
1254+
#
1255+
# RSpec.describe "The admin page", :type => :request do
1256+
# it "can be viewed by admins" do
1257+
# login_with admin_user
1258+
# get "/admin"
1259+
# expect(response).to be_ok
1260+
# end
1261+
#
1262+
# it "cannot be viewed by guests" do
1263+
# login_with guest_user
1264+
# get "/admin"
1265+
# expect(response).to be_forbidden
1266+
# end
1267+
# end
1268+
#
1269+
# @note Filtered context inclusions can also be applied to
1270+
# individual examples that have matching metadata. Just like
1271+
# Ruby's object model is that every object has a singleton class
1272+
# which has only a single instance, RSpec's model is that every
1273+
# example has a singleton example group containing just the one
1274+
# example.
1275+
#
1276+
# @see #include
1277+
def include_context(shared_group_name, *filters)
1278+
shared_module = world.shared_example_group_registry.find([:main], shared_group_name)
1279+
include shared_module, *filters
11811280
end
11821281

11831282
# Tells RSpec to extend example groups with `mod`. Methods defined in
@@ -1211,9 +1310,9 @@ def include(mod, *filters)
12111310
# @see #include
12121311
# @see #prepend
12131312
def extend(mod, *filters)
1214-
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1215-
@extend_modules.append(mod, meta)
1216-
on_existing_matching_groups(meta) { |group| safe_extend(mod, group) }
1313+
define_mixed_in_module(mod, filters, @extend_modules, :extend) do |group|
1314+
safe_extend(mod, group)
1315+
end
12171316
end
12181317

12191318
if RSpec::Support::RubyFeatures.module_prepends_supported?
@@ -1250,9 +1349,9 @@ def extend(mod, *filters)
12501349
# @see #include
12511350
# @see #extend
12521351
def prepend(mod, *filters)
1253-
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1254-
@prepend_modules.append(mod, meta)
1255-
on_existing_matching_groups(meta) { |group| safe_prepend(mod, group) }
1352+
define_mixed_in_module(mod, filters, @prepend_modules, :prepend) do |group|
1353+
safe_prepend(mod, group)
1354+
end
12561355
end
12571356
end
12581357

@@ -1261,6 +1360,8 @@ def prepend(mod, *filters)
12611360
# Used internally to extend a group with modules using `include`, `prepend` and/or
12621361
# `extend`.
12631362
def configure_group(group)
1363+
group.hooks.register_globals(group, hooks)
1364+
12641365
configure_group_with group, @include_modules, :safe_include
12651366
configure_group_with group, @extend_modules, :safe_extend
12661367
configure_group_with group, @prepend_modules, :safe_prepend
@@ -1270,7 +1371,8 @@ def configure_group(group)
12701371
#
12711372
# Used internally to extend the singleton class of a single example's
12721373
# example group instance with modules using `include` and/or `extend`.
1273-
def configure_example(example)
1374+
def configure_example(example, example_hooks)
1375+
example_hooks.register_global_singleton_context_hooks(example, hooks)
12741376
singleton_group = example.example_group_instance.singleton_class
12751377

12761378
# We replace the metadata so that SharedExampleGroupModule#included
@@ -1956,6 +2058,16 @@ def safe_extend(mod, host)
19562058
end
19572059
# :nocov:
19582060
end
2061+
2062+
def define_mixed_in_module(mod, filters, mod_list, config_method, &block)
2063+
unless Module === mod
2064+
raise TypeError, "`RSpec.configuration.#{config_method}` expects a module but got: #{mod.inspect}"
2065+
end
2066+
2067+
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
2068+
mod_list.append(mod, meta)
2069+
on_existing_matching_groups(meta, &block)
2070+
end
19592071
end
19602072
# rubocop:enable Metrics/ClassLength
19612073
end

lib/rspec/core/example.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ def duplicate_with(metadata_overrides={})
142142
new_metadata, new_metadata[:block])
143143
end
144144

145+
# @private
146+
def update_inherited_metadata(updates)
147+
metadata.update(updates) do |_key, existing_example_value, _new_inherited_value|
148+
existing_example_value
149+
end
150+
end
151+
145152
# @attr_reader
146153
#
147154
# Returns the first exception raised in the context of running this
@@ -229,8 +236,7 @@ def example_group
229236
def run(example_group_instance, reporter)
230237
@example_group_instance = example_group_instance
231238
@reporter = reporter
232-
hooks.register_global_singleton_context_hooks(self, RSpec.configuration.hooks)
233-
RSpec.configuration.configure_example(self)
239+
RSpec.configuration.configure_example(self, hooks)
234240
RSpec.current_example = self
235241

236242
start(reporter)

lib/rspec/core/example_group.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -364,16 +364,16 @@ def self.remove_example(example)
364364

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

369-
unless shared_block
369+
unless shared_module
370370
raise ArgumentError, "Could not find shared #{label} #{name.inspect}"
371371
end
372372

373-
SharedExampleGroupInclusionStackFrame.with_frame(name, Metadata.relative_path(inclusion_location)) do
374-
module_exec(*args, &shared_block)
375-
module_exec(&customization_block) if customization_block
376-
end
373+
shared_module.include_in(
374+
self, Metadata.relative_path(inclusion_location),
375+
args, customization_block
376+
)
377377
end
378378

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

427427
@currently_executing_a_context_hook = false
428428

429-
hooks.register_globals(self, RSpec.configuration.hooks)
430429
RSpec.configuration.configure_group(self)
431430
end
432431

@@ -690,6 +689,17 @@ class << self; self; end
690689
# :nocov:
691690
end
692691

692+
# @private
693+
def self.update_inherited_metadata(updates)
694+
metadata.update(updates) do |_key, existing_group_value, _new_inherited_value|
695+
existing_group_value
696+
end
697+
698+
RSpec.configuration.configure_group(self)
699+
examples.each { |ex| ex.update_inherited_metadata(updates) }
700+
children.each { |group| group.update_inherited_metadata(updates) }
701+
end
702+
693703
# Raised when an RSpec API is called in the wrong scope, such as `before`
694704
# being called from within an example rather than from within an example
695705
# group block.

0 commit comments

Comments
 (0)