Skip to content

Instantly share code, notes, and snippets.

@woods
Last active November 7, 2017 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save woods/ab8cc6efaaa0726e23827187cc7e2a07 to your computer and use it in GitHub Desktop.
Save woods/ab8cc6efaaa0726e23827187cc7e2a07 to your computer and use it in GitHub Desktop.
West Arete's spec layout

Over time, West Arete has developed a convention for how we organize our spec directories. This arrangement has been very successful for us over many years and many projects. We'd love to extract it into some kind of framework/gem, but have never managed/invested to do so. So at the very least we can share this writeup of what we've found to be useful, in the hope that it's useful to others.

The top level of our spec directory looks like this:

spec/
  spec_helper.rb
  component/
  factories/
  integration/
  unit/

You could use whatever high-level directories you want -- e.g. "models", "views", "controllers". The point is that you want each subtree to load a slightly different spec configuration.

The spec/spec_helper.rb file is almost completely empty. It only contains general settings for rspec. Here's an example:

# This is the most general RSpec configuration for the project.
#
# This file should only contain spec setup code that is common to all types
# of specs. In particular, it should *not* load Rails or the database, since
# that would add that overhead to the lightweight unit tests in the suite.

RSpec.configure do |config|

  # Force new `expect(object)` syntax and disallow old `object.should` syntax.
  config.expect_with :rspec do |c|
    c.syntax = :expect
  end

end

# Define the root of the project (for specs that don't load Rails).
ROOT = Pathname.new(File.expand_path(File.dirname(__FILE__) + '/..'))

Then each subdirectory gets its own spec helper, which only loads specific config files from the "support" directory:

spec/
  component/
    component_spec_helper.rb
    controllers/
    helpers/
    models/
      user_spec.rb
      page_spec.rb
      ...

The spec/component/component_spec_helper looks like this:

require 'spec_helper'
require 'support/rails'
require 'support/factory_girl'
require 'support/devise'
require 'support/database_cleaner'
require 'support/mail'
require 'support/matchers/have_db_foreign_key'
require 'support/i18n'

Note that its only job is to load support files in a specific order. We never put configuration code in here, in order to keep it modular.

Finally, each support file covers a specific concern of configuration. Here's our spec/support/rails.rb:

ENV["RAILS_ENV"] ||= 'test'
puts "Loading Rails..."
require "#{ROOT}/config/environment"
require 'rspec/rails'
require 'shoulda/matchers'

RSpec.configure do |config|
  # Automatically assign spec type based on directory naming convention.
  config.infer_spec_type_from_file_location!

  # Allow rspec to use named routes
  config.include Rails.application.routes.url_helpers
end

And here's spec/support/capybara.rb:

require 'capybara/rspec'
require 'capybara/dsl'

# Tell capybara to use css selectors, as opposed to xpath
Capybara.default_selector = :css

# Ajax can run a little slowly on some machines. Give it a chance.
Capybara.default_max_wait_time = 5

RSpec.configure do |config|
  config.include(Capybara::DSL, type: :request)
end

This way, configuration concerns are isolated, and can be shared between different rspec sub-environments. And the order that configurations are loaded is very clear.

This also means that if you just run bin/rspec spec/unit, then it runs very, very fast because it doesn't have to load Rails or clean the database (those are only needed for component and integration tests). And if you run bin/rspec spec/component, then it starts faster because it doesn't have to load the integration test stuff.

And if you just run bin/rspec, then it does load all the configurations and run all the tests.

The only caveat is if you have an rspec setup that has global state that is totally incompatible with another rspec environments. We run into this on our system tests that use capybara/poltergeist/phantomjs to remotely exercise a staging server after deployment. To get that to work, we need to set global rspec variables that would clash with the unit/component/integration tests, so we put those system tests in a top-level spec.system directory that requires running them in a totally separate process with bin/rspec spec.system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment