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