RSpec Style Guide & Best Practices
This skill applies the RSpec Style Guide and Better Specs. RuboCop RSpec enforces many of these rules.
Use when writing or reviewing RSpec examples, request specs, model specs, or integration tests.
Layout
- •No empty line after
describe/context/featureopening — start the block body immediately. - •One empty line between sibling
describe/contextblocks; none after the last block in a group. - •One empty line after
subject,let, andbefore/afterblocks. - •Group
subjectandletblocks together; separate them frombefore/afterwith a blank line. - •One empty line around each
it/specifyblock to separate expectations from context.
# good
describe Article do
subject { FactoryBot.create(:article) }
let(:user) { FactoryBot.create(:user) }
before do
# ...
end
describe '#summary' do
context 'when there is a summary' do
it 'returns the summary' do
# ...
end
end
context 'when there is no summary' do
it 'returns nil' do
# ...
end
end
end
end
Example Group Structure
- •Order: Declare in this order:
subject, thenlet!/let, thenbefore/after. Putsubjectfirst when used. - •Contexts: Use
contextto group examples. Start with when, with, or without. Prefer having a matching negative case (e.g. "when X" and "when not X"). - •let / let!: Use
letfor data shared across examples; uselet!when the value must exist before the example runs (e.g. for scopes). Preferletover instance variables (@var). - •Shared examples: Use
shared_examples/it_behaves_liketo DRY repeated behavior. Don’t over-DRY early; duplication in specs is acceptable for clarity. - •Hooks: Don’t specify
:each(it’s the default). Use:contextinstead of:allwhen you need scope; avoidbefore(:context)when possible (state leakage). - •No
itin iterators: Don’t generate examples in a loop; write eachdescribe/itexplicitly so changes are localized.
# bad
[:new, :show, :index].each do |action|
it 'returns 200' do
get action
expect(response).to be_ok
end
end
# good – separate describe per action
describe 'GET new' do
it 'returns 200' do
get :new
expect(response).to be_ok
end
end
Describe Your Methods
Use the Ruby documentation convention: . (or ::) for class methods, # for instance methods.
# bad describe 'the authenticate method for User' describe 'if the user is an admin' # good describe '.authenticate' describe '#admin?'
Use Contexts
Group with context; full example names (all block descriptions concatenated) should read as a sentence.
# bad
it 'has 200 status code if logged in' do
expect(response).to respond_with 200
end
# good
context 'when logged in' do
it { is_expected.to respond_with 200 }
end
context 'when logged out' do
it { is_expected.to respond_with 401 }
end
Subject
- •Use
subjectwhen several examples relate to the same object under test. - •Prefer named subject when you reference it:
subject(:article) { ... }andexpect(article).to .... Use anonymoussubjectonly when you useis_expectedand never reference the subject by name. - •In nested contexts, if you reassign subject with different data, give it a different name (e.g.
guest_article) so intent is clear. - •Don’t stub the subject: If you stub methods on the object under test, fix the design (e.g. inject dependencies or use a different subject) instead.
# good – named subject
describe Article do
subject(:article) { FactoryBot.create(:article) }
it 'is not published on creation' do
expect(article).not_to be_published
end
end
# good – anonymous when using is_expected
context 'when not valid' do
it { is_expected.to respond_with 422 }
end
Example Structure
- •Expectation per example: In isolated unit specs, prefer one expectation per example. For non-isolated specs (DB, HTTP, integration), multiple expectations in one example are acceptable; consider
:aggregate_failureswhen you have several. - •Example descriptions: Don’t end with a conditional (e.g. "returns X if Y"); put the condition in a
contextand keep the example description about the outcome. Keep descriptions under ~60 characters; usecontextto split. - •No "should" in descriptions: Use third person, present tense. Don’t start with "should" or "should not".
# bad
it 'should return the summary'
it 'returns the summary if it is present'
# good
context 'when display name is present' do
it 'returns the display name' do
# ...
end
end
it 'returns the summary'
it 'does not change timings'
Expect Syntax
Use expect only; never should. For one-liners with implicit subject, use is_expected.to.
# bad
response.should respond_with_content_type(:json)
it { should respond_with 422 }
# good
expect(response).to respond_with_content_type(:json)
it { is_expected.to respond_with 422 }
Configure RSpec to enforce expect syntax:
# spec_helper.rb or rails_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
let and let!
Prefer let over before { @var = ... }. Use let! when the value must exist before the example (e.g. testing scopes or queries). Don’t overuse let for trivial primitives; balance reuse vs. clarity.
# bad
before { @resource = FactoryBot.create(:device) }
# good
let(:resource) { FactoryBot.create(:device) }
let!(:user) { FactoryBot.create(:user) } # when you need it loaded before the example
Matchers
- •Predicate matchers: Prefer RSpec’s predicate matchers:
expect(article).to be_publishedinstead ofexpect(article.published?).to be true. - •Built-in matchers: Use built-in matchers (e.g.
include,eq) instead of hand-rolled expectations. - •Avoid bare
be: Don’t useexpect(x).to be; usebe_truthy,be_nil, or a specific matcher (e.g.be_an(Author)). - •Custom matchers: Extract repeated expectation logic into custom matchers (or use libraries like Shoulda Matchers).
- •No
any_instance_of: Avoidallow_any_instance_of/expect_any_instance_of; stub or inject the dependency instead. - •Block expectations: Prefer explicit block form:
expect { do_something }.to change(something).to(new_value)over implicit subject with a lambda.
# bad expect(article.published?).to be true expect(article.author).to be # good expect(article).to be_published expect(article.author).to be_truthy expect(article.author).to be_an(Author)
Doubles and Stubbing
- •Verifying doubles: Prefer
instance_double,object_double,class_double, and verifying partial doubles over non-verifying doubles. Keepverify_partial_doublesenabled. - •Don’t stub subject: Don’t stub methods on the object under test; fix design or use a different subject (e.g. a presenter that receives the collaborator).
- •Mock sparingly: Prefer real behavior when possible; use doubles to isolate external dependencies (DB, HTTP). If stubbing could hide a real bug, you’ve gone too far.
- •Constants: Don’t define classes/modules/constants inside example groups (they leak). Use
stub_constor anonymousClass.newand assign to alet.
Time and HTTP
- •Time: Use Timecop (or
ActiveSupport::Testing::TimeHelpers#freeze_time) instead of stubbingTime.now/Date. - •HTTP: Stub external HTTP (e.g. WebMock, VCR) so specs don’t hit real services.
Data and Factories
- •Needed data only: Create only the data the example needs. Avoid dozens of records unless the test truly needs them.
- •Factories over fixtures: Use Factory Bot (or similar), not fixtures. In unit tests, prefer minimal setup or objects built in the spec.
- •Incidental state: Avoid depending on incidental state (e.g. "there are exactly 2 articles"); use
change(Article, :count).by(1)or similar so the example is self-contained.
Rails: Integration, Controllers, Models, Views, Mailers
- •Integration over controller specs: Prefer integration/request specs that test behavior; don’t add controller specs just for coverage if integration tests suffice.
- •Controllers: Mock models and stub their methods; test only controller responsibility (assigns, redirects, template, status). Use contexts for success vs. failure (e.g. "when the article saves" / "when the article fails to save").
- •Models: Don’t mock the model under test. Use
FactoryBot.createor a new instance. Add an example that the factory-built model is valid. For validations, useexpect(model.errors[:attr].size).to eq(1)(or a matcher) so the right attribute is validated. Name the other object in uniqueness specsanother_article(or similar). - •Views: Mock models in view specs. Use
assignfor instance variables. Prefer Capybara negative selectors (have_no_selector) overnot_to have_selector. Stub helpers ontemplatewhen the view uses them. - •Mailers: Mock the model; verify subject, from, to, and body content.
Summary Checklist
- • Layout: no empty line after describe/context; one between sibling groups; blank after let/subject/before; one around each it
- • Order: subject, then let/let!, then before/after
- • Describe with
.methodor#method; context with when/with/without - • Short example descriptions; no "should"; no conditional in the it string (use context)
- • One expectation per example in unit specs; use expect / is_expected only
- • Named subject when referenced; don’t stub subject
- • Prefer verifying doubles; no any_instance_of
- • Time: Timecop/freeze_time; HTTP: WebMock/VCR
- • Minimal data; factories not fixtures; no it in iterators
- • Shared examples where they reduce duplication without obscuring intent
References
- •RSpec Style Guide — layout, structure, naming, matchers, Rails
- •Better Specs — Rails testing practices (Lelylan Labs)
- •RSpec · RuboCop RSpec