Skip to content

Commit f470ce3

Browse files
committed
Include shared context safely with improved before/after hooks. (#42)
* Add before/after/around helpers. * Make it possible to include/prepend shared modules.
1 parent ffdf1f3 commit f470ce3

File tree

5 files changed

+100
-6
lines changed

5 files changed

+100
-6
lines changed

lib/sus/context.rb

+52
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,57 @@ def each(&block)
7575
end
7676
end
7777
end
78+
79+
# Include an around method to the context class, that invokes the given block before running the test.
80+
#
81+
# Before hooks are called in the reverse order they are defined, in other words the last defined before hook is called first.
82+
#
83+
# @parameter hook [Proc] The block to execute before each test.
84+
def before(&hook)
85+
wrapper = Module.new
86+
87+
wrapper.define_method(:around) do |&block|
88+
instance_exec(&hook)
89+
super(&block)
90+
end
91+
92+
self.include(wrapper)
93+
end
94+
95+
# Include an around method to the context class, that invokes the given block after running the test.
96+
#
97+
# After hooks are called in the order they are defined, in other words the last defined after hook is called last.
98+
#
99+
# @parameter hook [Proc] The block to execute after each test. An `error` argument is passed if the test failed with an exception.
100+
def after(&hook)
101+
wrapper = Module.new
102+
103+
wrapper.define_method(:around) do |&block|
104+
error = nil
105+
106+
super(&block)
107+
rescue => error
108+
raise
109+
ensure
110+
instance_exec(error, &hook)
111+
end
112+
113+
self.include(wrapper)
114+
end
115+
116+
# Add an around hook to the context class.
117+
#
118+
# Around hooks are called in the reverse order they are defined.
119+
#
120+
# The top level `around` implementation invokes before and after hooks.
121+
#
122+
# @paremeter block [Proc] The block to execute around each test.
123+
def around(&block)
124+
wrapper = Module.new
125+
126+
wrapper.define_method(:around, &block)
127+
128+
self.include(wrapper)
129+
end
78130
end
79131
end

lib/sus/include_context.rb

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
module Sus
99
module Context
10+
# Include a shared context into the current context, along with any arguments or options.
11+
#
12+
# @parameter shared [Sus::Shared] The shared context to include.
13+
# @parameter arguments [Array] The arguments to pass to the shared context.
14+
# @parameter options [Hash] The options to pass to the shared context.
1015
def include_context(shared, *arguments, **options)
1116
self.class_exec(*arguments, **options, &shared.block)
1217
end

lib/sus/it_behaves_like.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ def self.build(parent, shared, arguments = nil, unique: false, &block)
1818
base.description = shared.name
1919
base.identity = Identity.nested(parent.identity, base.description, unique: unique)
2020
base.set_temporary_name("#{self}[#{base.description}]")
21-
21+
2222
# User provided block is evaluated first, so that it can provide default behaviour for the shared context:
2323
if block_given?
2424
base.class_exec(*arguments, &block)
2525
end
26-
26+
2727
base.class_exec(*arguments, &shared.block)
2828
return base
2929
end

lib/sus/shared.rb

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ module Shared
1111
attr_accessor :block
1212

1313
def self.build(name, block)
14-
base = Class.new
14+
base = Module.new
1515
base.extend(Shared)
1616
base.name = name
1717
base.block = block
1818

1919
return base
2020
end
21+
22+
def included(base)
23+
base.class_exec(&self.block)
24+
end
25+
26+
def prepended(base)
27+
base.class_exec(&self.block)
28+
end
2129
end
2230

2331
def self.Shared(name, &block)

test/sus/include_context.rb

+32-3
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,47 @@
33
# Released under the MIT License.
44
# Copyright, 2023, by Samuel Williams.
55

6-
AThing = Sus::Shared("a thing") do |key, value: 42|
6+
AContextWithArguments = Sus::Shared("a context with arguments") do |key, value: 42|
77
let(:a_thing) {{key => value}}
88
end
99

10+
AContextWithHooks = Sus::Shared("a context with hooks") do
11+
before do
12+
events << :shared_before
13+
end
14+
15+
after do
16+
events << :shared_after
17+
end
18+
19+
around do |&block|
20+
events << :shared_around_before
21+
super(&block)
22+
end
23+
end
24+
1025
describe Sus::Context do
1126
with '.include_context' do
1227
with "a shared context with an option" do
13-
include_context AThing, :key, value: 42
28+
include_context AContextWithArguments, :key, value: 42
1429

15-
it "can include a shared context" do
30+
it "can include a shared context with arguments" do
1631
expect(a_thing).to be == {:key => 42}
1732
end
1833
end
34+
35+
with "a shared context with arguments" do
36+
let(:events) {Array.new}
37+
38+
include AContextWithHooks
39+
40+
before do
41+
events << :example_before
42+
end
43+
44+
it "can include a shared context" do
45+
expect(events).to be == [:example_before, :shared_around_before, :shared_before]
46+
end
47+
end
1948
end
2049
end

0 commit comments

Comments
 (0)