(I'm using JRuby, but I think that my problem applies to MRI Ruby as well)
My RSpec definition has this overall structure:
RSpec.describe 'XXX' do
before(:all) do
# preparation common for each example
end
after(:all) do
# clean up after each example
end
context 'C1' do
it 'Ex1' do
....
end
it 'Ex2' do
....
end
....
end
context 'C2' do
.... # more examples here
end
end
The problem here is that the preparation work (before(:all)
) involves calculating a value, say
some_value=MyMod.foo()
which I would need in the cleanup code (after(:all)
), i.e.
MyMod.bar(some_value)
Of course, since some_value
is a local variable, I can not do this. In theory, I could use a global variable,
before(:all) do
$some_value=MyMod.foo()
end
after(:all) do
MyMod.bar($some_value)
end
but this is not only ugly, it would cause problems if I decide one day to parallelize my tests.
Can this be achieved? Or is my overall design flawed?
UPDATE : As requested in a comment, I explain here why I want to do this:
Our application has a set of global properties (technically speaking, they are stored in a global singleton object), and these "properties" are set to default values according to the specification of this application, i.e. in the normal case, they are never changed. However, for purpose of experimentation, it is possible to override these properties during startup of the application (via environment variables).
Hence the vast majority of RSpec tests just use those defaults. However, some unit tests also are supposed to test the case where these properties have a value different from the default. Currently I write the examples so that at the start of the example, I store the current test of the property in a variable, and at the end of the exmple set the porperty back to the original value. If I want to do this for a set of examples, which are scoped in the same describe
or context
block, I thought it would be better to do this change-and-reset of the variables in a before
or after
block, so that it is done uniformly in all examples in this group.
As already noted by Stefan in the comments, you can use instance variables for this.
Specifically, each RSpec example group (i.e. describe
or context
block) defines a class, and any examples (it
, example
, specify
, etc.) and hooks (before
, after
and around
) in the group are evaluated in the context of an instance of that class.
Thus, as the documentation shows, you can define an instance variable (which will belong to that instance of the example group class) in a before
block and access it from any it
or after
blocks in the same group (or in nested groups, which inherit hooks from their parent groups), e.g. like this:
describe 'something' do
before(:each) do
@some_value = MyMod.foo()
end
after(:each) do
MyMod.bar(@some_value)
end
# Add your examples here...
end
(You can also use before(:all)
and after(:all)
here, in which case the hooks are only executed once for the entire example group, rather than before and after each example in the group. Other than that, they work exactly the same. Note that before(:suite)
hooks, however, can not be used to define instance variables like this, since they are evaluated differently. In general, I'd recommend just not using them.)
An alternative solution is to use the let!
to define a memoized helper method that (because of the !
) is automatically evaluated before any examples, and whose saved value is still available later:
describe 'something' do
# This is evaluated in an implicit before hook before each example:
let!(:some_value) { MyMod.foo() }
after(:each) do
MyMod.bar(some_value)
end
# Add your examples here...
end
One advantage of this approach (besides arguably cleaner syntax with less @
signs) is that some_value
cannot be reassigned in examples (which could lead to subtle bugs). However, its contents may of course still include mutable objects, so this does not perfectly guarantee that the settings restored in the after
block are still the same as what they were before the examples.
Also, while you can set multiple instance variables in a single before
block, let!
only lets you define one helper method. Of course you could always work around that limitation with something like this:
describe 'something' do
let!(:saved_settings) do
some_value = MyMod.foo()
other_value = MyMod.foo2()
[some_value, other_value].freeze
end
after(:each) do
some_value, other_value = saved_settings
MyMod.bar(some_value)
MyMod.bar2(other_value)
end
end
One more (minor) limitation of this approach is that let!
is evaluated before each example, so you cannot have it evaluated only once per example group like before(:all)
. However, it's generally good practice to clean up the environment after each example anyway, so in practice this isn't as much of a limitation as it might seem.
Yet another solution, already suggested by Todd A. Jacobs in the comments, is of course to use an around
hook instead:
describe 'something' do
around(:each) do |example|
some_value = MyMod.foo()
example.run
MyMod.bar(some_value)
end
# Add your examples here...
end
Arguably this is the neatest and most flexible method. However, it does have the same limitation as let!
in that the hook will always be executed once for each example; there's no support for around(:all)
hooks in RSpec. At least not without additional third party gems.
(Disclaimer: I have not tested the gem linked above, I just found it with a quick Google search. However, at a glance the code looks solid, even if it hasn't been updated for a decade.)
Ps. One common situation that resembles your example is where you want to change the value of a Ruby constant before your tests (e.g. to toggle a feature flag, replace a config setting or even mock an entire class) and restore its original value afterwards. For that, RSpec Mocks provides a specific solution in the form of the stub_const
method:
describe 'something' do
before do
stub_const('MyMod::FOO', 42)
end
# Add your examples here...
end
The code above ensures that MyMod::FOO
will have the value 42 within your examples, while also automatically restoring its original value after each example. Note that you don't even have to define an after
hook for restoring the constant to its original value, since RSpec Mocks takes care of that automatically.
You can also call stub_const
directly from an example (it
/ example
/ specify
) block, which reduces boilerplate code even further if you only need the constant stubbed for a single example (or if you need to give it different values in different examples).