Fall leaves pexels photo

A useful feature of RSpec is the ability to pass metadata to tests and suites.

You may already be familiar with how Capybara uses the :js option to enable the javascript driver.

describe "a javascript feature", :js do
  # tests run against the Capyabara.javascript_driver
end

Capybara provides an RSpec configuration hook that changes the web driver for any example where :js metadata is present. Here it is, oversimplified:

# capybara/rspec.rb
RSpec.configure do |config|
  config.before do
    Capybara.current_driver = Capybara.javascript_driver if example.metadata[:js]
  end
end

We may reach a point in the maturity of our test suite when it makes sense add our own configuration options.

Changing Test Runner Behavior

Testing libraries like RSpec and Capybara do some heavy lifting to set up the Rails environment and make it suitable for running in test mode. For performance reasons, it may be beneficial to run each of our specs in a database transaction so test data can be easily rolled back at the start of each spec.

Here's a common base configuration for using the popular DatabaseCleaner gem to set up transactional database behavior for RSpec:

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Not all specs can be run this way - once we add a javascript acceptance specs, for example, the javascript driver will likely need its own connection to the database so it won't have access to data setup in the tests. We need to run javascript acceptance specs in truncation mode to ensure database changes are committed to the database so multiple database connections will have access to the same data.

Let's use RSpec metadata to toggle database behavior automatically when using the javascript driver (i.e., not the default :rack_test driver). We'll add the following hooks, borrowed from the DatabaseCleaner README:

# spec/spec_helper.rb
config.before(:each, type: :feature) do
  # :rack_test driver's Rack app under test shares database connection
  # with the specs, so continue to use transaction strategy for speed.
  driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

  if !driver_shares_db_connection_with_specs
    # Driver is probably for an external browser with an app
    # under test that does *not* share a database connection with the
    # specs, so use truncation strategy.
    DatabaseCleaner.strategy = :truncation
  end
end

We also run into problems with ActiveRecord after_commit callbacks - when running tests in transaction mode, these callbacks will never fire. We can also add an option for enabling truncation mode outside of acceptance specs when isolated specs are needed for these callbacks:

# spec/model/user_spec.rb
it "triggers background job after creating new user", :truncation_mode do
  # test after_commit callback
end

# spec/spec_helper.rb
config.before(:each, :truncation_mode) do
  DatabaseCleaner.strategy = :truncation
end

Here's a consolidated configuration for providing hooks for the issues related to database truncation mentioned above:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, type: :feature) do
    driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    if !driver_shares_db_connection_with_specs
      DatabaseCleaner.strategy = :truncation
    end
  end

  config.before(:each, :truncation_mode) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Changing Application Settings

Rails provides a number of settings that can be easily configured based on the environment, so we avoid undesired work in development or test environments, such as sending emails. For any mature Rails application, we'll likely have our own custom settings layered on top of the Rails defaults.

There are many cases where we'll still want to test the production settings in our test environments. For example, by default, controller caching is disabled in tests:

# config/initializers/test.rb
Rails.application.configure do
  # ...
  config.action_controller.perform_caching = false

end

For selected acceptances specs, we may still want to test behavior of caching at the view layer, say that users can see new info when a model attribute changes. We don't need this caching behavior is all test, so it may be useful to toggle specs on/off during the test run.

First attempt

We could try to stub the setting in the context of a single spec run with the enabled state.

# spec/spec_helper.rb
RSpec.configure do |config|
  config.before(:each, :caching) do
    allow_any_instance_of(ActionController::Base).to receive(:perform_caching).and_return true
  end

  config.after(:each, :caching) do
    Rails.cache.clear
  end
end

This may require changing behavior of instances which is typically discouraged. We may also need to clean up other global state, like clearing the Rails cache after the test run.

Better attempt

Alternatively, we can set the actual values on while settings are derived. Here's how it might look for enabling controller caching with an around block:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.around(:each, :caching) do |example|
    caching = ActionController::Base.perform_caching
    ActionController::Base.perform_caching = example.metadata[:caching]

    example.run

    Rails.cache.clear
    ActionController::Base.perform_caching = caching
  end
end

The around block takes the RSpec example object as an argument. When running specs, the given block is triggered when :caching is detected as a key in an example’s metadata. The example object provides a number of methods for test introspection, allowing us to make changes before and after calling run to execute the spec.

As a result, we now have a simple, explicit mechanism for introducing caching to individual specs and suites:

# spec/features/homepage_spec.rb
describe "visit the homepage", :caching do
  it "expires cache" do
    # test cached stuff
  end
end

The main concern with this approach is that modifying a global state can affect other tests unintentionally - a big no-no.

To avoid this, we need to reset the original value when the example completes.

Here, we are storing the previously set value of ActionContoller::Base.perform_caching, setting it for the local suite, and resetting it back to the original value after it completes.

This technique may come into play when integrating with certain gems like PaperTrail which may generate expensive logic or queries not need in most situations. PaperTrail even provides a helper to take advantage of RSpec. It may be worth considering whether to provide an interface to toggle behavior and RSpec helpers next time we write a gem. metadata to toggle behavior in specs.

Filtering Specs

One useful technique while developing is to run a selected set of specs. We may be editing acceptances specs, model validations, and other disparate tests while test driving a feature from outside to in.

Manual tagging

Adding arbitrary metadata like :focus to set of specs is one way to approach this.

# spec/models/user_spec.rb
it "validates a user", :focus do
  # unit test
end

# spec/features/sign_up_spec.rb
it "displays error message", :focus do
  # acceptance spec
end

We can now filter our test run to a subset at the command line:

$ rspec --tag focus

We can also add some global configuration so this will be the default behavior when using :focus specs, as long as we don't make the mistake of filtering on the build server unintentionally.

RSpec.configure do |config|
  # enable auto-focus only when running locally
  config.filter_run_including focus: ENV['CI_SERVER_SETTING'].blank?

  config.run_all_when_everything_filtered = true
end

Alternatively, avoid running broken or flaky specs when tagged accordingly:

it "test that fails intermittently", :flaky do
  # probably a javascript test
end

Using either a command line option

$ rspec --tag ~flaky

or a configuration option, we can filter out specs we wish to ignore.

RSpec.configure do |c|
  c.filter_run_excluding flaky: true
end

Auto Tagging

A less-known feature of RSpec 3 is an API for telling RSpec to derive additional metadata automatically based on other metadata.

For example, each spec example has metadata that includes its file path. This, along with the RSpec::Core::Configuration#define_derived_metadata method, allows us to alter spec behavior based on the spec directories, for example.

Why is this useful and how do we use it? Glad you asked.

Let's say we want to isolate model specs that require database truncation since they are more like functional specs than unit specs. We may set up our spec directory like so:

spec/
  truncation/
    example1_spec.rb
    example2_spec.rb
    ...
  transaction/
    example1_spec.rb
    example2_spec.rb
    ...

Instead of manually tagging each file with our :truncation_mode metadata we used earlier to toggle DatabaseCleaner's truncation strategy, we can configure all the specs in spec/truncation as follows:

# spec/spec_helper.rb
RSpec.configure do |config|
  config.define_derived_metadata(file_path: %r{spec/truncation}) do |metadata|
    metadata[:truncation_mode] = true
  end

  # rest of DatabaseCleaner config below
end

Now, all specs in the directory will run with the :truncation_mode metadata and the database strategy will be set to :truncation as long as it is declared ahead of the additional DatabaseCleaner configuration we referenced earlier.

Note, this is the same method used in rspec-rails to add custom behavior to specs in the specific directories, e.g. spec/controllers',spec/requests, etc.

Using and Abusing

While using RSpec metadata can be a powerful technique for altering test behavior and application settings in specs, it can also be taken too far.

As @avdgaag notes in his blog post on the topic, make sure to distinguish between how spec is run from what the spec should test. We should probably not use metadata to create records specific to certain tests or authenticate users for a given context, for example.

One rule of thumb for adding metadata is decide whether it would be generally useful to any Rails app (good) or it is specific to the business logic of your current application (bad). The latter is best set up more explicitly within or alongside your tests.

Before considering a new metadata tag, I ask the rubber duck "Could I extract this configuration into a gem?" To answer yes, the behavior would have to be non-specific to my application. If so, the behavior might be useful as metadata.

While metadata can nicely separate the boilerplate required to setup and teardown test behavior, it also adds a layer of indirection that can cause readability issues when stretched too far. Understand that there is a big increase in mental overhead to permuting test behavior with each new tag option and consider the tradeoffs with the rest of the team.

Use wisely!


Did you like this post? Do me a favor: share it on Twitter, follow me - @rossta, and sign up for my newsletter. Thanks!

Published on Dec 29, 2015