RSpec Coder
Core Philosophy
- AAA Pattern: Arrange-Act-Assert structure for clarity
- Behavior over Implementation: Test what code does, not how
- Isolation: Tests should be independent
- Descriptive Names: Blocks should clearly explain behavior
- Coverage: Test happy paths AND edge cases
- Fast Tests: Minimize database operations
- Fixtures: Use fixtures for common data setup
- Shoulda Matchers: Use for validations and associations
Critical Conventions
❌ Don't Add require 'rails_helper'
RSpec imports via .rspec config. Adding manually is redundant.
# ✅ GOOD - no require needed
RSpec.describe User do
# ...
end
❌ Don't Add Redundant Spec Type
RSpec infers type from file location automatically.
# ✅ GOOD - type inferred from spec/models/ location
RSpec.describe User do
# ...
end
✅ Use Namespace WITHOUT Leading ::
# ✅ GOOD - no leading double colons
RSpec.describe DynamicsGp::ERPSynchronizer do
# ...
end
Test Organization
File Structure
spec/models/ - Model unit tests
spec/services/ - Service object tests
spec/controllers/ - Controller tests
spec/requests/ - Request specs (API testing)
spec/mailers/ - Mailer tests
spec/jobs/ - Background job tests
spec/fixtures/ - Test data
spec/support/ - Helper modules and shared examples
spec/rails_helper.rb - Rails-specific configuration
Using describe and context
| Block |
Purpose |
Example |
describe |
Groups by method/class |
describe "#process" |
context |
Groups by condition |
context "when user is admin" |
RSpec.describe OrderProcessor do
describe "#process" do
context "with valid payment" do
# success tests
end
context "with invalid payment" do
# failure tests
end
end
end
Subject and Let
See [references/patterns.md](references/patterns.md) for detailed examples.
| Pattern |
Use Case |
subject(:name) { ... } |
Primary object/method under test |
let(:name) { ... } |
Lazy-evaluated, memoized data |
let!(:name) { ... } |
Eager evaluation (before each test) |
RSpec.describe User do
describe "#full_name" do
subject(:full_name) { user.full_name }
let(:user) { users(:alice) }
it { is_expected.to eq("Alice Smith") }
end
end
Fixtures
See [references/patterns.md](references/patterns.md) for detailed examples.
# spec/fixtures/users.yml
alice:
name: Alice Smith
email: [email protected]
admin: false
RSpec.describe User do
fixtures :users
it "validates email" do
expect(users(:alice)).to be_valid
end
end
Mocking and Stubbing
See [references/patterns.md](references/patterns.md) for detailed examples.
| Method |
Purpose |
allow(obj).to receive(:method) |
Stub return value |
expect(obj).to receive(:method) |
Verify call happens |
# Stubbing external service
allow(PaymentGateway).to receive(:charge).and_return(true)
# Verifying method called
expect(UserMailer).to receive(:welcome_email).with(user)
Matchers Quick Reference
See [references/matchers.md](references/matchers.md) for complete reference.
Essential Matchers
# Equality
expect(value).to eq(expected)
# Truthiness
expect(obj).to be_valid
expect(obj).to be_truthy
# Change
expect { action }.to change { obj.status }.to("completed")
expect { action }.to change(Model, :count).by(1)
# Errors
expect { action }.to raise_error(SomeError)
# Collections
expect(array).to include(item)
expect(array).to be_empty
Shoulda Matchers
# Validations
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:email) }
# Associations
it { is_expected.to have_many(:posts) }
it { is_expected.to belong_to(:account) }
AAA Pattern
Structure all tests as Arrange-Act-Assert:
describe "#process_refund" do
subject(:process_refund) { processor.process_refund }
let(:order) { orders(:completed_order) }
let(:processor) { described_class.new(order) }
it "updates order status" do
process_refund # Act
expect(order.reload.status).to eq("refunded") # Assert
end
it "credits user account" do
expect { process_refund } # Act
.to change { order.user.reload.account_balance } # Assert
.by(order.total)
end
end
Test Coverage Standards
What to Test
| Type |
Test For |
| Models |
Validations, associations, scopes, callbacks, methods |
| Services |
Happy path, sad path, edge cases, external integrations |
| Controllers |
Status codes, response formats, auth, redirects |
| Jobs |
Execution, retry logic, error handling, idempotency |
Coverage Example
RSpec.describe User do
fixtures :users
describe "validations" do
subject(:user) { users(:valid_user) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
end
describe "associations" do
it { is_expected.to have_many(:posts).dependent(:destroy) }
end
describe "#full_name" do
subject(:full_name) { user.full_name }
let(:user) { User.new(first_name: "Alice", last_name: "Smith") }
it { is_expected.to eq("Alice Smith") }
context "when last name is missing" do
let(:user) { User.new(first_name: "Alice") }
it { is_expected.to eq("Alice") }
end
end
end
Anti-Patterns
See [references/anti-patterns.md](references/anti-patterns.md) for detailed examples.
| Anti-Pattern |
Why Bad |
require 'rails_helper' |
Redundant, loaded via .rspec |
type: :model |
Redundant, inferred from location |
Leading :: in namespace |
Violates RuboCop style |
| Empty test bodies |
False confidence |
| Testing private methods |
Couples to implementation |
| Not using fixtures |
Slow tests |
| Not using shoulda |
Verbose validation tests |
Best Practices Checklist
Critical Conventions:
Test Organization:
Test Data:
Assertions:
Coverage:
Quick Reference
# Minimal spec file
RSpec.describe User do
fixtures :users
describe "#full_name" do
subject(:full_name) { user.full_name }
let(:user) { users(:alice) }
it { is_expected.to eq("Alice Smith") }
end
describe "validations" do
subject(:user) { users(:alice) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to have_many(:posts) }
end
end