rubyrspecjruby

Passing information from 'before' to 'after' block


(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.


Solution

  • 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).