Document Explicit Dependencies Through Tests

Document Explicit Dependencies Through Tests

One of the purposes of writing tests is to provide living documentation of application’s code. Tests provide real examples of how a certain class or function is supposed to be used. Tests could also document the exact dependencies of the tested code.

The Problem

When Rails boots it loads most, if not all, of the application’s code, along with all of the dependencies (gems). Because of this, there is no need to require dependencies in individual files that contain application logic. When looking at the source of a specific class, it is hard to tell what external code it depends on. The test doesn’t help either.

A typical RSpec test usually looks something like this:

require 'spec_helper'

describe StyleGuide do
  # actual tests omitted
end

In the Rails community, it has become a de facto standard to require the default spec_helper (or an equivalent) in each test file. A typical spec/spec_helper.rb file ends up loading the whole Rails environment, requiring numerous helper files, and setting up various configuration options. All of that, en masse, is more than what any particular test file needs.

Certainly, integration and feature tests depend on the entire Rails environment. ActiveRecord model tests depend on the presence and configuration of a database. These are all good use cases for spec_helper. But what about unit tests that don’t require the database? When testing a plain old Ruby class, there might only be a few dependencies, if any.

The Solution: RSpec 3.x rails_helper.rb and spec_helper.rb

RSpec 3.x introduces a new rails_helper.rb convention which contains all the Rails-specific spec configuration and leaves spec_helper.rb minimal, with no Rails code.

Here’s an example rails_helper.rb:

ENV["RAILS_ENV"] = "test"

require File.expand_path("../../config/environment", __FILE__)

require "rspec/rails"
require "shoulda/matchers"

Dir[Rails.root.join("spec/support/**/*.rb")].each { |file| require file }

module Features
  # Extend this module in spec/support/features/*.rb
  include Formulaic::Dsl
end

RSpec.configure do |config|
  config.include Features, type: :feature
  config.infer_base_class_for_anonymous_controllers = false
  config.infer_spec_type_from_file_location!
  config.use_transactional_fixtures = false
end

ActiveRecord::Migration.maintain_test_schema!
Capybara.javascript_driver = :webkit

Here’s an example spec_helper.rb:

require "webmock/rspec"

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.syntax = :expect
  end

  config.mock_with :rspec do |mocks|
    mocks.syntax = :expect
  end

  config.order = :random
end

WebMock.disable_net_connect!(allow_localhost: true)

Using the minimal spec_helper.rb

Being conscious about what must be required for a particular source file is a good thing. Instead of loading everything but the kitchen sink, let’s specify the minimum dependencies inside of the test.

Here’s a revision of our code from above:

require 'rubocop'
require 'app/models/style_guide'

describe StyleGuide do
  # actual tests omitted
end

This code states exactly what the tested class (app/models/style_guide.rb) depends on: the gem rubocop and the model style_guide.

Note that we don’t need to require spec_helper manually in either rails_helper or in our StyleGuide spec. It is required for us in .rspec by default:

--color
--warnings
--require spec_helper

The idea behind spec_helper is to keep it minimal and resist the urge to add things to it. It should avoid becoming a junk drawer.

Tests which might be part of a Rails application test suite, but don’t actually depend on Rails or ActiveRecord can now require this basic spec helper along with the essential gems and files.

Each test now explicitly documents dependencies of the tested code. Loading minimal dependencies during tests removes any magical coupling, helps with refactoring, saves time during debugging, and makes tests run faster.

What’s Next?

Want to learn more techniques about decoupling code away from Rails?

Greg Lazarev Developer