RSpec ⛑

Hero image for RSpec ⛑

To test Ruby applications, we prefer using RSpec over MiniTest but restrain ourselves to its core features and use the following best practices.

Formatting

  • Use a single, top-level describe ClassName block, with appropriate type.
RSpec.describe SomeClass, type: model do; end
RSpec.describe SomeClass, type: system do; end
  • Order validation, association, and method tests in the same order that they appear in the class.

  • Use describe to group tests by method-under-test.

    1. Use . (or ::) when referring to a class method’s name.
    2. Use # when referring to an instance method’s name.
# Bad
describe 'the confirm method of User'
describe 'search method of User'

# Good
describe '#confirm'
describe '.search'
  • Use context to describe testing preconditions. context block descriptions should always start with ‘when’ or ‘given’, and be in the form of a sentence with proper grammar.
# Bad
it 'processes payment if the params are valid'
it 'declines payment if the params are invalid'

# Good
context 'when params are valid' do
  it 'processes payment if the params are valid'
end

context 'when params are invalid' do
  it 'declines payment'
end
  • Use subject blocks to define objects for use in one-line specs only.
# BAD
context 'when using an explicit subject' do
  subject { 'foo' }

  it 'should equal foo' do
    # it's not okay to use `subject` here:
    expect(subject).to eq('foo')
  end
end
  
# GOOD
context 'when defining a subject' do
  # it's okay to define a `subject` here:
  subject { 'foo' }

  it { expect(subject).to eq('foo') }
end
  • Put one-liner specs at the beginning of the outer describe blocks.
# Bad
RSpec.describe 'validations' do
  it 'validates uniqueness of name' do
    Fabricate(:place, name: 'Bangkok')

    place_with_same_name = Fabricate.build(:place, name: 'Bangkok')

    expect(place_with_same_name).not_to be_valid
  end
  
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:slug) }
end

# Good
RSpec.describe 'validations' do
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:slug) }
  
  it 'validates uniqueness of name' do
    Fabricate(:place, name: 'Bangkok')

    place_with_same_name = Fabricate.build(:place, name: 'Bangkok')

    expect(place_with_same_name).not_to be_valid
  end
end
  • Always use the expect syntax.
# Bad
it 'creates a resource' do
  response.should respond_with_content_type(:json)
end

# Good
it 'creates a resource' do
  expect(response).to respond_with_content_type(:json)
end
  • Don’t prefix it block descriptions with should. Use the imperative tone instead.
# Bad
it 'should process payment'

# Good
it 'processes payment'

should is an artifact from a older Rspec version (< 3)

  • Do not leave line breaks after context or describe blocks.
# Bad
RSpec.describe Place, type: :model do

  describe '#create' do

    it 'should create a place' do

      # ...
    end
  end
end

# Good
RSpec.describe Place, type: :model do
  describe '#create' do
    it 'should create a place' do
      # ...
    end
  end
end
  • Leave one line return around it blocks.
# Bad
describe '#summary' do
  it 'returns the summary' do
    # ...
  end
  it 'does something else' do
    # ...
  end
  it 'does another thing' do
    # ...
  end
end

# Good
describe '#summary' do
  it 'returns the summary' do
    # ...
  end
  
  it 'does something else' do
    # ...
  end
  
  it 'does another thing' do
    # ...
  end
end

Fixtures

Factories

  • Use a gem like fabrication or factory_bot to reduce the verbosity when working with models.
# Bad
user = User.create( :name => "Genoveffa",
                    :surname => "Piccolina",
                    :city => "Billyville",
                    :birth => "17 Agoust 1982",
                    :active => true)

# Good
user = Factory.create(:user)

Fixtures isolation

  • DO NOT use let, let! to create fixtures and limit usage of before and after.

  • Instead, each test must create its own fixtures and setup.

# Bad
RSpec.describe Article do
  # arrange
  let(:user) { FactoryGirl.create(:user) }
  
  before do
    # arrange
  end
  
  after do
    # cleanup
  end
  
  describe '#summary' do
    # act
    # assertion     
  end
end

# Good
RSpec.describe Article do
  describe '#summary' do
    # arrange
    user = FactoryGirl.create(:user)
    
    # act
    
    # assertion
  end
end

Keep creation of fixtures to a minimum

  • Test suites can become very slow due to haphazard fixtures creation.

  • Try to make the tests atomic and as such avoid unnecessary fixtures creations.

# Bad
it 'updates the cache counter' do
  place = Fabricate(:place)
  # Unnecessary creation of 5 records
  Fabricate.times(5, :checkin, place: place)
    
  expect(place.checkins_count).to eq 5
end

# Good
it 'updates the cache counter' do
  place = Fabricate(:place)
  Fabricate(:place, :checkin, place: place)
    
  expect(place.checkins_count).to eq 1
end

Assertion

Single expectation test

  • Each test should make only one assertion.
# Bad
it 'creates a resource' do
  expect(response).to respond_with_content_type(:json)
  expect(response).to assign_to(:resource)
end

# Good
it { is_expected.to respond_with_content_type(:json) }
it { is_expected.to assign_to(:resource) }
  • For integration tests, sometimes the tests may suffer a performance hit especially when setup is complex. In those cases, it’s fine to assert more than one behavior.

Test all possible cases

  • Use Boundary value analysis technique to test valid, edge and invalid cases.

  • Split-up method’s input or object’s attributes into valid and invalid partitions and test both of them and their boundaries.

# Bad
RSpec.describe '#month_in_english' do
  context 'when valid' do
    it 'should return 'January' for 1' # lower boundary
  end
  context 'when invalid' do
    it 'should return nil for 0'
  end
end

# Good
RSpec.describe '#month_in_english' do
  context 'when valid' do
    it 'should return January for 1' # lower boundary
    it 'should return March for 3'
    it 'should return December for 12' # upper boundary
  end
  context 'when invalid' do
    it 'should return nil for 0'
    it 'should return nil for 13'
  end
end

Incidental State

  • Avoid incidental state as much as possible.
# Bad
it 'publishes the article' do
  article.publish

  # Creating another shared Article test object above would cause this
  # test to break
  expect(Article.count).to eq(2)
end

# Good
it 'publishes the article' do
  expect { article.publish }.to change(Article, :count).by(1)
end

Readable matchers

  • Use readable matchers and double check the available rspec matchers.
# Bad
lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound

# Good
expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
  • Avoid checking boolean equality directly.
# Class under test:

class Thing
  def awesome?
    true
  end
end

# Bad
it 'is true' do
  thing = Thing.new
  expect(thing.awesome?).to eq(true)
end

# Good
it 'is true' do
  thing = Thing.new
  expect(thing).to be_awesome
end

DRY-ing up tests

  • Avoid using Shared Examples as much as possible. These types of tests do have their place in a few instances but should not be the goto method to write tests.

  • Extract reusable code into helper methods.

# Bad

# spec/systems/user_signs_in_spec.rb
RSpec.describe 'User can sign in', type: :system do
  scenario 'logs a user into the application' do
    user = Fabricate(:user)
    
    visit root_path
    
    fill_in 'user_session_email',    with: user.email
    fill_in 'user_session_password', with: user.password
    click_button "Sign in"

    expect(page).to have_content "Your account"
  end
end

# Good

# spec/systems/user_signs_in_spec.rb
RSpec.describe 'User can sign in', type: :system do
  it 'logs a user into the application' do
    sign_in

    expect(page).to have_content "Your account"
  end
end

# spec/support/authentication_helper.rb
module AuthenticationHelper
  def sign_in
    user = Fabricate(:user)
    
    visit root_path

    fill_in 'user_session_email',    with: user.email
    fill_in 'user_session_password', with: user.password
    click_button "Sign in"
  end
end

Test Doubles

  • Use stubs, mocks and spies to isolate the test to the object under test.

  • When resorting to mocking and stubbing, only mock against a small, stable, and obvious (or documented) API, so stubs are likely to represent reality after future refactoring.

  • Avoid stubbing a method to a level where it could give a false-positive test result.

Stubbing HTTP requests

  • Stub all network requests. Whenever possible, prefer using VCR.
# Bad
it 'creates a new answer' do
  user = Fabricate(:user)
  
  # makes a network request
  service = described_class.new(service_params(user.id))
  
  expect(service.call).to be_a(Api::V1::ForumAnswer)
end

# Good
it 'creates a new answer', vcr: 'api/forums/valid-answer' do
  user = Fabricate(:user)
  
  # network request is stubbed by VCR
  service = described_class.new(service_params(user.id))
  
  expect(service.call).to be_a(Api::V1::ForumAnswer)
end

When using VCR is not option, we recommend creating a light mock web server using Sinatra.