2 Comments

Using the RSpec Around Hooks

A couple of weeks back, I was working on some RSpec tests that required a different setup and tear-down than other tests in the app. We had database cleaner providing cleanup for us to prevent test interaction, but certain tests we created needed a heavier cleanup than others. We had a simple before(:each) and after(:each) with the setup and tear-down, but that had to change to accommodate certain types of tests.

The Setup

We have defined RSpec metadata on certain tests that need extra attention in the setup or tear-down stages like so:

describe 'the test name here', type: :spec_type do
...
end

This allows us to filter (RSpec filtering in before/after hooks) the tests with type :spec_type in our spec_helper.rb file. The :spec_type is arbitrary in this case, it could be anything (:unit, :special_test, :things_need_more_cleaning, etc). In our spec_helper.rb we can now target these tests for extra work with befores and afters:

RSpec.configure do |config|
  config.before(:each, type: :spec_type) { # special setup }
  config.after(:each, type: :spec_type) { # special teardown }
end

Looking back at our project, we had a few of these guys set up to provide database cleanup between tests, but they didn’t care about the test type — we had only one need fore cleanup. After introducing a couple of new test types, we had need for different cleanup methods, so we would have written this:

RSpec.configure do |config|
  config.before(:each) { # general setup }
  config.after(:each) { # general tear-down }
  config.before(:each, type: :special_type) { # special setup }
  config.after(:each, type: :special_type) { # special tear-down }
  config.before(:each, type: :fancy_type) { # special setup }
  config.after(:each, type: :fancy_type) { # special tear-down }
end

This example shows a general setup for any test type, but for some cases of test types it had some special hooks. In this case, the :special_type and :fancy_type shared the same setup and tear-down — not very DRY, huh? After a little looking, we found the RSpec around hook.

RSpec Around Hook Explained

The around hook allows us to DRY up the setup and tear-down of our tests. DRYing up the tests is not the only thing around() lets us do; it also allows us to wrap our before and afters with another layer of setup/teardown. This is how we came out:

RSpec.configure do |config|
  config.before(:each) { # other setup }
  config.after(:each) { # other tear-down }
  config.around(:each) do |example|
    if [:special_type, :fancy_type].include?(example.metadata[:type])
      #special setup
    else
      #general setup
    end
    
    example.run
    
    if [:special_type, :fancy_type].include?(example.metadata[:type])
      #special tear-down
    else
      #general tear-down
    end
  end
end

Our sample would run in this order:

  1. around each setup (general or special setup — dependent on the if statement)
  2. before each
  3. the actual spec
  4. after each
  5. around each tear-down (general or special tear-down — dependent on the if statement)

There are plenty of possibilities with the around, before, and after hooks, especially since they can be targeted to all tests, a suite of tests, and each test.