The code that supports tests is often neglected when it is time to refactor rails code. I suspect this is because developers have limited time for refactoring and they feel it’s better spent on implementation code.

Although those priorities aren’t wrong, I think it’s valuable at least once in the lifespan of a project to invest some time in cleaning up your test environment. That way you can accumulate optimizations and good practices for each new application you work with.

In rails applications that use RSpec the rails_helper (née: spec_helper) file provides configuration for the application’s specs.

In a moderately complex application, this might include configuration for Sidekiq, DatabaseCleaner, the javascript driver (EG: Poltergeist), and many other utilities.

Usually, developers will just dump the configuration for these utilities in their various before, after, and around hooks that litter the rails_helper file.

In this post I’ll outline a simple way that you can organise your rails_helper file so that it is easier to read and maintain.

Organising rails_helper

Let’s have a look at what a rails_helper might look like in a Rails codebase:

require 'spec_helper'

# Some crufty requires omitted here

RSpec.configure do |config|

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = false

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  # rspec-rails 3 will no longer automatically infer an example group's spec type
  # from the file location. You can explicitly opt-in to the feature using this
  # config option.
  config.infer_spec_type_from_file_location!

  config.include ActiveSupport::Testing::TimeHelpers
  config.include(EmailSpec::Helpers)
  config.include(EmailSpec::Matchers)

  config.include ArchivableSpecHelper, type: :model
  config.include IssueSharedGroups,    type: :model

  config.include Devise::TestHelpers,    type: :controller
  config.include ControllerSharedGroups, type: :controller
  config.extend AuthenticationHelper,    type: :controller

  config.include Helpers::CurrentUserSupport, type: :view
  config.include ViewSharedGroups,            type: :view
  config.extend ViewMacros,                   type: :view

  config.include FeatureHelper,                        type: :feature
  config.extend Features::AuthenticationClassSupport,  type: :feature
  config.include Features::AuthenticationSupport,      type: :feature
  config.include Features::MembershipSelectionSupport, type: :feature
  config.include Features::RailsAdminArchiveSupport,   type: :feature
  config.include Features::RailsAdminDeleteSupport,    type: :feature
  config.include Features::UserSupport,                type: :feature

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

  config.after(:each) do |example|
    if example.exception && example.metadata[:js]
      meta = example.metadata
      filename = File.basename(meta[:file_path])
      line_number = meta[:line_number]
      screenshot_name = "screenshot-#{filename}-#{line_number}.png"
      screenshot_path = "#{Rails.root.join("tmp")}/#{screenshot_name}"
      html_name = "page-#{filename}-#{line_number}.html"
      html_path = "#{Rails.root.join("tmp")}/#{html_name}"
      page.save_page(html_path)
      page.save_screenshot(screenshot_path)
      puts meta[:full_description] + "\n  HTML: #{html_path}"
      puts meta[:full_description] + "\n  Screenshot: #{screenshot_path}"
    end
  end

  config.around(:each, js: true) do |ex|
    DatabaseCleaner.strategy = :truncation
    ex.run
    DatabaseCleaner.strategy = :transaction
  end

  config.around(:each, truncation: true) do |ex|
    DatabaseCleaner.strategy = :truncation
    ex.run
    DatabaseCleaner.strategy = :transaction
  end

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

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

### START - Sidekiq configuration for tests
  config.before(:each) do
    # Clears out the jobs for tests using the fake testing
    Sidekiq::Worker.clear_all
  end

  config.around(:each) do |example|
    if example.metadata[:sidekiq] == :fake
      Sidekiq::Testing.fake!(&example)
    elsif example.metadata[:sidekiq] == :inline
      Sidekiq::Testing.inline!(&example)
    elsif example.metadata[:type] == :feature
      Sidekiq::Testing.inline!(&example)
    else
      Sidekiq::Testing.fake!(&example)
    end
  end
### END - Sidekiq configuration for tests

  # Carrierwave settings for testing
  config.after(:all) do
    # See initializers/carrierwave.rb
    uploaded_files_path = Rails.root.join("public", "test_assets")
    FileUtils.rm_r(uploaded_files_path) if File.exists?(uploaded_files_path)
  end
end

Oh boy. This configuration file suffers from a poor signal to noise ratio. It’s really hard to tell what is being configured, particularly if I’m just looking for the configuration of just one utility.

In the case of the rails_helper above, the solution is to break each set of configuration items out into its own file. That would give us the following support files:

# spec/capybara_screenshot_support.rb
RSpec.configure do |config|
  config.after(:each) do |example|
    if example.exception && example.metadata[:js]
      meta = example.metadata
      filename = File.basename(meta[:file_path])
      line_number = meta[:line_number]
      screenshot_name = "screenshot-#{filename}-#{line_number}.png"
      screenshot_path = "#{Rails.root.join("tmp")}/#{screenshot_name}"
      html_name = "page-#{filename}-#{line_number}.html"
      html_path = "#{Rails.root.join("tmp")}/#{html_name}"
      page.save_page(html_path)
      page.save_screenshot(screenshot_path)
      puts meta[:full_description] + "\n  HTML: #{html_path}"
      puts meta[:full_description] + "\n  Screenshot: #{screenshot_path}"
    end
  end
end

# spec/carrierwave_support.rb
RSpec.configure do |config|
  config.after(:all) do
    # See initializers/carrierwave.rb
    uploaded_files_path = Rails.root.join("public", "test_assets")
    FileUtils.rm_r(uploaded_files_path) if File.exists?(uploaded_files_path)
  end
end

# spec/database_cleaner_support.rb
RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each, js: true) do |ex|
    DatabaseCleaner.strategy = :truncation
    ex.run
    DatabaseCleaner.strategy = :transaction
  end

  config.around(:each, truncation: true) do |ex|
    DatabaseCleaner.strategy = :truncation
    ex.run
    DatabaseCleaner.strategy = :transaction
  end

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

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

# spec/sidekiq_support.rb
RSpec.configure do |config|
  config.before(:each) do
    # Clears out the jobs for tests using the fake testing
    Sidekiq::Worker.clear_all
  end

  config.around(:each) do |example|
    if example.metadata[:sidekiq] == :fake
      Sidekiq::Testing.fake!(&example)
    elsif example.metadata[:sidekiq] == :inline
      Sidekiq::Testing.inline!(&example)
    elsif example.metadata[:type] == :feature
      Sidekiq::Testing.inline!(&example)
    else
      Sidekiq::Testing.fake!(&example)
    end
  end
end

…and an updated rails_helper file that looks like:

require 'spec_helper'
# Some crufty requires omitted here

# Utility specific configuration
require 'capybara_screenshot_support'
require 'carrierwave_support'
require 'database_cleaner_support'
require 'sidekiq_support'

RSpec.configure do |config|

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = false

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  # rspec-rails 3 will no longer automatically infer an example group's spec type
  # from the file location. You can explicitly opt-in to the feature using this
  # config option.
  config.infer_spec_type_from_file_location!

  config.include ActiveSupport::Testing::TimeHelpers
  config.include(EmailSpec::Helpers)
  config.include(EmailSpec::Matchers)

  config.include ArchivableSpecHelper, type: :model
  config.include IssueSharedGroups,    type: :model

  config.include Devise::TestHelpers,    type: :controller
  config.include ControllerSharedGroups, type: :controller
  config.extend AuthenticationHelper,    type: :controller

  config.include Helpers::CurrentUserSupport, type: :view
  config.include ViewSharedGroups,            type: :view
  config.extend ViewMacros,                   type: :view

  config.include FeatureHelper,                        type: :feature
  config.extend Features::AuthenticationClassSupport,  type: :feature
  config.include Features::AuthenticationSupport,      type: :feature
  config.include Features::MembershipSelectionSupport, type: :feature
  config.include Features::RailsAdminArchiveSupport,   type: :feature
  config.include Features::RailsAdminDeleteSupport,    type: :feature
  config.include Features::UserSupport,                type: :feature
end

Now that each configuration has been encapsulated in its own file, it’s easy to configure individual utilities. It’s also easy to enable and disable a configuration by toggling whether the file is required.

Although it seems like an obvious change to make, a lot of projects still have cluttered rails_helper files. If you have one, I’d suggest you go ahead and take the five minutes it costs to split it up. It’ll make your life easier.