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 { 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.