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 appropriatetype
.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.- Use . (or ::) when referring to a class method’s name.
- 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 withshould
. 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
ordescribe
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
orfactory_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 ofbefore
andafter
. -
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.