Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Testing strategies with RSpec

These are some tricks, tips, strategies, lessons learnt from a year of working with RSpec on Rails projects. Rails convention over configuration has proved fruitful for adoption and standardisation but there times when you do want to tweak certain things.  We’ll highlight some ways how that can be done such as changing the directory structure of your tests.

RSpec Directory Structure

Coming from a Java background, I was used to see all of a projects tests in the ‘test’ directory with subdirectories off of those for certain types of tests such as ‘unit’ and ‘integration’.  Using libraries like rspec and rspec-rails sets certain expectations on where certain types of tests will be. Model tests will be in ‘spec/models’, controllers in ‘spec/controllers’. This has been great importing useful helpers for those given situations, but it doesn’t tell me if those are unit tests or something else.

From rspec-rails 2.4 tags such as “type: :controller” can be used to import the appropriate helpers and the tests can be placed where they fit best for the application. I would recommend the following:

spec/
  ./unit
    ./models
    ./controllers
    ./etc...
  ./integration
    ./models
    ./etc...
  ./acceptance

The spec files themselves would look like:

describe FooModel, type: :model do
…
end

RSpec 2.12 (current latest stable) supports tags for model, controller, helper and routing.

Unit testing without a database

Why is it important to do unit testing without hitting a database? If a test hits a database then the execution path is through your business logic, through a persistence library, perhaps a database adapter and then the database. A unit test should be focused on the smallest unit of execution, namely the response and messages sent by a class method. Besides testing this whole stack, there typically tends to be an order of magnitude more unit tests in a project than any other type of test and if the tests are hitting the database hundreds or thousands of times this will eventually lead to a slow test suite.

One way of ensuring unit tests do not hit a database in Rails is to use a null object pattern database adapter. The best known is nulldb.  Although the latest stable release doesn’t support Rails 3.1+ adding the HEAD sha as a dependency will support the very latest Rails projects. The only place an application should really be hitting the database is within the application models so the nulldb adapter can be set for those types of tests.

In Gemfile:

gem 'activerecord-nulldb-adapter', git: 'git://github.com/nulldb/nulldb.git'

In spec/spec_helper.rb:

RSpec.configure do |config|
  ...
  config.before(type: :model) do
    require 'nulldb_rspec'
    ActiveRecord::Base.establish_connection :adapter => :nulldb
  end
  ...
end

If an application is integrating with a software as a service the result is the same. Unit tests should not be hitting that API, stubs should be used to mock the dependency. One way to ensure the tests are not hitting an API would be to open the class that hits the API and overwrite any methods that the application uses and raise an exception.

class ApiClient
  def do_something
    raise “Whoa there, you shouldn’t be here!”
  end
end

If an application does need to sometimes hit that API and sometimes not, RSpec allows models to be included depending on the tag. This would allow some test groups to use the API and others to be intercepted and raise the unexpected visitor error.

config.include ApiInterceptors, group: :model

What’s in an integration test

The integration tests are the best place for testing database integrations like scopes, or complex queries. They are also a good place for testing integrations with other libraries or services such as a message queue, background worker or remote API. An application may also want to test that the layers of an application are integrated correctly. After all with all that stubbing in the unit tests it can mislead a developer as to the correctness of an application. If an application has observers or is event driven this would be a good point to see if those layers are integrated successfully.

Sharing behaviours for integration and acceptance testing

If a project is using the BDD language in stories, the application may be reusing those in its acceptance tests. These stories are often the application codified in human language. Where they are used in the application they are often used in the acceptance testing stage but it doesn’t have to be constrained to there. If those stories are written without specifying the how it can be easier to share, e.g. “a user creates a foo widget” rather than “a user clicks on the ‘create foo widget’; a user fills in ‘bar’ for the foo widget; a user presses the create button’.

RSpec has a concept of ‘shared examples’ which can be used to share behaviours between testing stages. If the test reads in such a way where it could be run against a browser, command line or API, all of those test layers could use the behaviour.

In spec/features/foo_example.rb:

shared_examples "foo" do
  describe "user creates a foo” do
    it "lists new foos" do
      given_user_exists
      when_user_creates_a_foo
      then_foo_is_listed_against_user
    end
  end
end

Then in the target test file, including the shared example will bring that test in and be run in that context with associated tags.

in spec/integration/foo_spec.rb

require 'spec_helper'

def given_user_exists
end

def when_user_creates_a_foo
end

def then_foo_is_listed_against_user
end

describe 'Foo', type: :foo do
  include_examples "a foo"
end

The method calls can be defined so they are inline and in scope, this way each file has its own implementation of the requirement methods for the test. The methods can also be extracted into a module and included through RSpec configuration.

in spec/spec_helper.rb

config.include(IntegrationHelpers, type: :foo)

in spec/support/integration_helpers.rb

module IntegrationHelpers
  def given_user_exists
  end
  def when_user_creates_a_foo
  end
  def then_foo_is_listed_against_user
  end
end

Following this pattern means the business language can be added to code and used in multiple places, and therefore stages of the testing.

Comments
  1. Luca Guidi says:

    I’ve just started to use brand new RSpec “feature” type (there is a convention for this within rspec-rails) in combination with Capybara. This experiment is supposed to replace Cucumber, but I’m having some issues with sessions.

    I have a “step” that performs the login (filling the form, hitting the submit button etc..). Well, everything works fine, but if I visit a page just after, it turns out that the session isn’t initialized yet (#“).

    Have you encountered this problem?

  2. Patrick says:

    I’m pretty curious about integration tests that are not full stack, when they are needed or not, etc. To me, integration tests for a method of a class often feel like a duplicate of the unit tests for that method, except slower because it touches the database. Wondering what opinions other developers have on this.

  3. Robbie Clutton says:

    For me the integration is testing the dependency which could be a database or an API. Say if there’s some non-trivial SQL/AREL, I would want to actually put some records in, run that query and make sure I get the correct records back. I would also want to stub out interactions with an API but I’d also want to have test coverage of consuming that API. That may not run every test run, perhaps it only runs in CI not locally to protect for regressions or breaking changes. It could also run nightly instead of every time so as not to slow development down.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *