RSpec ⛑

Hero image for RSpec ⛑

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

All general testing best practices and Web testing best practices apply to RSpec but working with a framework adds another superset of conventions to follow.

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 { is_expected.to validate_presence_of(:name) }
      it { is_expected.to validate_presence_of(:slug) }
    end
    
    # Good
    RSpec.describe 'validations' do
      it { is_expected.to validate_presence_of(:name) }
      it { is_expected.to 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 'creates a place' do
    
          # ...
        end
      end
    end
    
    # Good
    RSpec.describe Place, type: :model do
      describe '#create' do
        it 'creates 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 'returns "January" for 1' # lower boundary
      end
      context 'when invalid' do
        it 'returns nil for 0'
      end
    end
    
    # Good
    RSpec.describe '#month_in_english' do
      context 'when valid' do
        it 'returns "January" for 1' # lower boundary
        it 'returns "March" for 3'
        it 'returns "December" for 12' # upper boundary
      end
      context 'when invalid' do
        it 'returns nil for 0'
        it 'returns 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
    
  • Use named HTTP status like :no_content over its numeric representation. See a list of supported HTTP status codes

    # Bad
    expect(response).to have_http_status(204)
    
    # Good
    expect(response).to have_http_status(:no_content)
    

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.