4 ways to refactor and speed up RSpec tests

Ruby on Rails / RSpec

Tests are very important part of every app so it is also important to write them very well. However, sometimes is hard to keep the test suite clean and fast, especially when multiple developers were working before you or are working with you on the project. In this article I will focus on improving tests in two main ways: refactoring common parts of the test that are repeated many times and speeding up the test by reducing database requests. Ready?

Shared examples

Let’s consider the following example:

class AgePolicy
  def old_enough?(age)
    age >= 18
  end
end

it’s a simple policy that helps us to check if given user is old enough to take some actions in our app. We have a very simple test for this class:

require 'spec_helper'

describe AgePolicy do
  describe '#old_enough?' do
    it 'returns false if user is 16 years old' do
      policy = AgePolicy.new

      expect(policy.old_enough?(16)).to eq(false)
    end

    it 'returns false if user is 12 years old' do
      policy = AgePolicy.new

      expect(policy.old_enough?(12)).to eq(false)
    end

    it 'returns true if user is 18 years old' do
      policy = AgePolicy.new

      expect(policy.old_enough?(18)).to eq(true)
    end

    it 'returns true if user is 20 years old' do
      policy = AgePolicy.new

      expect(policy.old_enough?(20)).to eq(true)
    end
  end
end

As you can see the only thing that is dynamic in our tests is the age. This is the situation when we can use shared_examples feature provided by RSpec test framework:

require 'spec_helper'

describe AgePolicy do
  describe '#old_enough?' do
    shared_examples 'user eligible for taking an action' do |age|
      it "returns true if user is #{age} years old" do
        policy = AgePolicy.new

        expect(policy.old_enough?(age)).to eq(true)
      end
    end

    shared_examples 'user not eligible for taking an action' do |age|
      it "returns false if user is #{age} years old" do
        policy = AgePolicy.new

        expect(policy.old_enough?(age)).to eq(false)
      end
    end

    it_behaves_like 'user not eligible for taking an action', 16
    it_behaves_like 'user not eligible for taking an action', 12
    it_behaves_like 'user eligible for taking an action', 18
    it_behaves_like 'user eligible for taking an action', 20
  end
end

Our test is a little bit faster but this was not our goal. Although our test is less readable now, we don’t repeat the same code in each example.

Custom matchers

RSpec has many useful matchers, we already used betruthy and befalsey in above example. Sometimes, when expecting given values we repeat the same code over and over again. I think one of the best examples is controller tests when we are testing response. Let’s consider the following example:

class SomeController
  def show
    render json: { success: true }
  end
end

How can we test if the response returns success attribute with the true as value? As following:

describe SomeController do
  describe 'GET #show' do
    it 'returns success response' do
      get :show, id: 11, format: :json

      expect(JSON.parse(response.body)).to eq({success: true})
    end
  end
end

wouldn’t be easier if you could write just this:

expect(response).to be_json_success

Is hard to tell what this matcher does only by looking at this line but if you have consistent logic across the whole app and you would create such matcher then you would know what it does. To create such matcher you have to create matchers.rb file (name it as you want) in spec/support directory and put there your matcher definition:

RSpec::Matchers.define :be_json_success do |expected|
  match do |actual|
    json_response = JSON.parse(actual.body)
    expect(json_response['success']).to eq(true)
  end
end

The last step is to require your matcher by including require 'support/matchers' in the spec_helper.rb file.

Remove not needed associations from factories

This method would be useful for you if you are using FactoryGirl gem in your application to deal with the real records in the database. Let’s say that you have User model and each user has one Contact and one Location record. You factory should be looking similar:

FactoryGirl.define do
  factory :user do
    contact
    location  
  end
end

Each time you use FactoryGirl.create :user or FactoryGirl.build :user then additional 2 records are created. Yes, even with build. If you don’t want to create any associated records I recommend FactoryGirl.build_stubbed :user instead.

Now imagine that you are using this base factory across all tests. If you really need both associations always in the database then it’s ok but if not then you have the chance to significantly improve the performance of your tests. Your base factory should always have only these attributes that are needed to pass validation. If you sometimes need associations then consider using traits:

FactoryGirl.define do
  factory :user do
    first_name { "John" }
    last_name { "Doe" }

    trait :with_location do
      location
    end
  end
end

and you can call your factory using FactoryGirl.create :user, :with_location

Use Rails transactional tests feature

How many times do you need only one record in the database for the whole test? Let’s use again user factory from the above paragraph and create a simple test:

require 'spec_helper'

describe User do
  let!(:user) { FactoryGirl.create :user }

  it 'does something' do
    # test with user
  end

  it 'does something' do
    # test with user
  end

  it 'does something' do
    # test with user
  end
end

How many User records are created in the database? One record for each example so the answer is 3 records. To avoid it you can use excellent letitbe helper from test-prof gem. This helper uses Rails transactional tests feature which means that the record is created only once at the beginning of the test and removed when the test is finished. Once you have gem installed you have to add require 'test_prof/recipes/rspec/let_it_be' line to your spec_helper.rb and helper is available:

require 'spec_helper'

describe User do
  let_it_be(:user) { FactoryGirl.create :user }

  it 'does something' do
    # test with user
  end

  it 'does something' do
    # test with user
  end

  it 'does something' do
    # test with user
  end
end

And you are done. If you have many examples using the same record then you can speed up given spec even by 50%. Remember that you can always use stubs if you don’t need any data in the database!