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

Commit bfc6652

Browse files
committed
Add config.when_first_matching_example_defined.
1 parent ceecf84 commit bfc6652

File tree

5 files changed

+213
-0
lines changed

5 files changed

+213
-0
lines changed

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Enhancements:
55

66
* Remove unneeded `:execution_result` example groups metadata, saving a
77
bit of memory. (Myron Marston, #2172)
8+
* Add new `config.when_first_matching_example_defined` hook. (Myron
9+
Marston, #2175)
810

911
Bug Fixes:
1012

features/.nav

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- before_and_after_hooks.feature
2121
- around_hooks.feature
2222
- filtering.feature
23+
- when_first_matching_example_defined.feature
2324
- subject:
2425
- implicit_subject.feature
2526
- explicit_subject.feature
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Feature: `when_first_matching_example_defined` hook
2+
3+
In large projects that use RSpec, it's common to have some expensive setup logic
4+
that is only needed when certain kinds of specs have been loaded. If that kind of
5+
spec has not been loaded, you'd prefer to avoid the cost of doing the setup.
6+
7+
The `when_first_matching_example_defined` hook makes it easy to conditionally
8+
perform some logic when the first example is defined with matching metadata,
9+
allowing you to ensure the necessary setup is performed only when needed.
10+
11+
Background:
12+
Given a file named "spec/spec_helper.rb" with:
13+
"""ruby
14+
RSpec.configure do |config|
15+
config.when_first_matching_example_defined(:db) do
16+
require "support/db"
17+
end
18+
end
19+
"""
20+
And a file named "spec/support/db.rb" with:
21+
"""ruby
22+
RSpec.configure do |config|
23+
config.before(:suite) do
24+
puts "Bootstrapped the DB."
25+
end
26+
27+
config.around(:example, :db) do |example|
28+
puts "Starting a DB transaction."
29+
example.run
30+
puts "Rolling back a DB transaction."
31+
end
32+
end
33+
"""
34+
And a file named ".rspec" with:
35+
"""
36+
--require spec_helper
37+
"""
38+
And a file named "spec/unit_spec.rb" with:
39+
"""
40+
RSpec.describe "A unit spec" do
41+
it "does not require a database" do
42+
puts "in unit example"
43+
end
44+
end
45+
"""
46+
And a file named "spec/integration_spec.rb" with:
47+
"""
48+
RSpec.describe "An integration spec", :db do
49+
it "requires a database" do
50+
puts "in integration example"
51+
end
52+
end
53+
"""
54+
55+
Scenario: Running the entire suite loads the DB setup
56+
When I run `rspec`
57+
Then it should pass with:
58+
"""
59+
Bootstrapped the DB.
60+
Starting a DB transaction.
61+
in integration example
62+
Rolling back a DB transaction.
63+
.in unit example
64+
.
65+
"""
66+
67+
Scenario: Running just the unit spec does not load the DB setup
68+
When I run `rspec spec/unit_spec.rb`
69+
Then the examples should all pass
70+
And the output should not contain "DB"

lib/rspec/core/configuration.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,41 @@ def define_derived_metadata(*filters, &block)
15971597
@derived_metadata_blocks.append(block, meta)
15981598
end
15991599

1600+
# Defines a callback that runs after the first example with matching
1601+
# metadata is defined. If no examples are defined with matching metadata,
1602+
# it will not get called at all.
1603+
#
1604+
# This can be used to ensure some setup is performed (such as bootstrapping
1605+
# a DB or loading a specific file that adds significantly to the boot time)
1606+
# if needed (as indicated by the presence of an example with matching metadata)
1607+
# but avoided otherwise.
1608+
#
1609+
# @example
1610+
# RSpec.configure do |config|
1611+
# config.when_first_matching_example_defined(:db) do
1612+
# # Load a support file that does some heavyweight setup,
1613+
# # including bootstrapping the DB, but only if we have loaded
1614+
# # any examples tagged with `:db`.
1615+
# require 'support/db'
1616+
# end
1617+
# end
1618+
def when_first_matching_example_defined(*filters, &block)
1619+
specified_meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1620+
1621+
callback = lambda do |example_or_group_meta|
1622+
# Example groups do not have `:example_group` metadata
1623+
# (instead they have `:parent_example_group` metadata).
1624+
return unless example_or_group_meta.key?(:example_group)
1625+
1626+
# Ensure the callback only fires once.
1627+
@derived_metadata_blocks.items_for(specified_meta).delete(callback)
1628+
1629+
block.call
1630+
end
1631+
1632+
@derived_metadata_blocks.append(callback, specified_meta)
1633+
end
1634+
16001635
# @private
16011636
def apply_derived_metadata_to(metadata)
16021637
@derived_metadata_blocks.items_for(metadata).each do |block|

spec/rspec/core/configuration_spec.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,111 @@ def exclude?(line)
15541554
end
15551555
end
15561556

1557+
describe "#when_first_matching_example_defined" do
1558+
include_examples "warning of deprecated `:example_group` during filtering configuration", :when_first_matching_example_defined
1559+
1560+
it "runs the block when the first matching example is defined" do
1561+
sequence = []
1562+
RSpec.configuration.when_first_matching_example_defined(:foo) do
1563+
sequence << :callback
1564+
end
1565+
1566+
RSpec.describe do
1567+
example("ex 1")
1568+
sequence << :before_first_matching_example_defined
1569+
example("ex 2", :foo)
1570+
sequence << :after_first_matching_example_defined
1571+
end
1572+
1573+
expect(sequence).to eq [:before_first_matching_example_defined, :callback, :after_first_matching_example_defined]
1574+
end
1575+
1576+
it "does not fire when later matching examples are defined" do
1577+
sequence = []
1578+
RSpec.configuration.when_first_matching_example_defined(:foo) do
1579+
sequence << :callback
1580+
end
1581+
1582+
RSpec.describe do
1583+
example("ex 1", :foo)
1584+
sequence.clear
1585+
1586+
sequence << :before_second_matching_example_defined
1587+
example("ex 2", :foo)
1588+
sequence << :after_second_matching_example_defined
1589+
end
1590+
1591+
expect(sequence).to eq [:before_second_matching_example_defined, :after_second_matching_example_defined]
1592+
end
1593+
1594+
it "does not run the block if no matching examples are defined" do
1595+
sequence = []
1596+
RSpec.configuration.when_first_matching_example_defined(:foo) do
1597+
sequence << :callback
1598+
end
1599+
1600+
RSpec.describe do
1601+
example("ex 1")
1602+
example("ex 2", :bar)
1603+
end
1604+
1605+
expect(sequence).to eq []
1606+
end
1607+
1608+
it 'does not run the block if groups match the metadata but no examples do' do
1609+
called = false
1610+
RSpec.configuration.when_first_matching_example_defined(:foo => true) do
1611+
called = true
1612+
end
1613+
1614+
RSpec.describe "group 1", :foo => true do
1615+
end
1616+
1617+
RSpec.describe "group 2", :foo => true do
1618+
example("ex", :foo => false)
1619+
end
1620+
1621+
expect(called).to be false
1622+
end
1623+
1624+
it "still runs after the first matching example even if there is a group that matches earlier" do
1625+
sequence = []
1626+
RSpec.configuration.when_first_matching_example_defined(:foo) do
1627+
sequence << :callback
1628+
end
1629+
1630+
RSpec.describe "group", :foo do
1631+
end
1632+
1633+
RSpec.describe do
1634+
example("ex 1")
1635+
sequence << :before_first_matching_example_defined
1636+
example("ex 2", :foo)
1637+
sequence << :after_first_matching_example_defined
1638+
end
1639+
1640+
expect(sequence).to eq [:before_first_matching_example_defined, :callback, :after_first_matching_example_defined]
1641+
end
1642+
1643+
context "when a group is defined with matching metadata" do
1644+
it "runs the callback after the first example in the group is defined" do
1645+
sequence = []
1646+
RSpec.configuration.when_first_matching_example_defined(:foo) do
1647+
sequence << :callback
1648+
end
1649+
1650+
sequence << :before_group
1651+
RSpec.describe "group", :foo do
1652+
sequence << :before_example
1653+
example("ex")
1654+
sequence << :after_example
1655+
end
1656+
1657+
expect(sequence).to eq [:before_group, :before_example, :callback, :after_example]
1658+
end
1659+
end
1660+
end
1661+
15571662
describe "#add_setting" do
15581663
describe "with no modifiers" do
15591664
context "with no additional options" do

0 commit comments

Comments
 (0)