name: rspec-testing-guidelines description: RSpec testing patterns, factories, mocks, and test best practices
RSpec Testing Guidelines
Purpose
Ensure comprehensive, maintainable test coverage using RSpec, FactoryBot, and supporting gems configured for the application.
When to Use This Skill
- Writing or modifying RSpec tests (model, request, system, etc.)
- Creating or using FactoryBot factories
- Setting up test data or fixtures
- Testing controllers, models, services, or views
- Using shoulda-matchers for cleaner assertions
- Testing Devise authentication flows
Test Suite Configuration
The app uses:
- RSpec for testing framework
- FactoryBot for test data
- SimpleCov for coverage reporting
- Shoulda Matchers for common Rails assertions
- Capybara + Selenium for system tests
- Devise Test Helpers for authentication
Quick Start: New Feature Testing Checklist
When adding a new feature:
- Create factory for new models
- Write model specs (validations, associations, methods)
- Write request specs for endpoints
- Write service/query specs if applicable
- Write system specs for user flows
- Verify test coverage with SimpleCov
Core Testing Principles
1. Follow AAA Pattern (Arrange-Act-Assert)
RSpec.describe UserRegistrationService do
describe '#call' do
# Arrange
let(:params) { { email: 'test@example.com', password: 'password' } }
subject(:service) { described_class.new(params) }
it 'creates a user' do
# Act
result = service.call
# Assert
expect(result).to be_success
expect(User.count).to eq(1)
end
end
end
2. Use FactoryBot Effectively
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password123' }
name { Faker::Name.name }
trait :admin do
role { 'admin' }
end
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
end
end
# Usage
user = create(:user) # Creates and saves
admin = create(:user, :admin) # With trait
user_with_posts = create(:user, :with_posts)
user_draft = build(:user) # Builds without saving
attrs = attributes_for(:user) # Returns hash
3. Test Behavior, Not Implementation
❌ Don't test private methods:
# Bad
describe '#normalize_email' do
it 'downcases email' do
user.send(:normalize_email)
expect(user.email).to eq('test@example.com')
end
end
✅ Do test public interface:
# Good
describe 'email normalization' do
it 'stores email in lowercase' do
user = create(:user, email: 'TEST@example.com')
expect(user.email).to eq('test@example.com')
end
end
4. Use Shoulda Matchers for Common Assertions
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# Associations
it { should have_many(:posts) }
it { should belong_to(:organization) }
# Validations
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
it { should validate_length_of(:password).is_at_least(8) }
# Database
it { should have_db_column(:email).of_type(:string) }
it { should have_db_index(:email) }
end
5. Use Contexts for Different Scenarios
RSpec.describe UsersController, type: :request do
describe 'POST /users' do
context 'when not authenticated' do
it 'redirects to login' do
post users_path, params: { user: attributes_for(:user) }
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when authenticated' do
before { sign_in create(:user) }
context 'with valid params' do
it 'creates a user' do
expect {
post users_path, params: { user: attributes_for(:user) }
}.to change(User, :count).by(1)
end
end
context 'with invalid params' do
it 'does not create a user' do
expect {
post users_path, params: { user: { email: 'invalid' } }
}.not_to change(User, :count)
end
it 'returns unprocessable entity status' do
post users_path, params: { user: { email: 'invalid' } }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
end
Test Types and When to Use Them
Model Specs (spec/models/)
Test validations, associations, scopes, and business logic.
# spec/models/post_spec.rb
RSpec.describe Post, type: :model do
describe 'associations' do
it { should belong_to(:user) }
it { should have_many(:comments) }
end
describe 'validations' do
it { should validate_presence_of(:title) }
end
describe 'scopes' do
describe '.published' do
let!(:published_post) { create(:post, :published) }
let!(:draft_post) { create(:post, :draft) }
it 'returns only published posts' do
expect(Post.published).to include(published_post)
expect(Post.published).not_to include(draft_post)
end
end
end
describe '#publish!' do
let(:post) { create(:post, :draft) }
it 'sets published to true' do
post.publish!
expect(post).to be_published
end
it 'sets published_at' do
freeze_time do
post.publish!
expect(post.published_at).to eq(Time.current)
end
end
end
end
Request Specs (spec/requests/)
Test HTTP endpoints, responses, and authentication.
# spec/requests/posts_spec.rb
RSpec.describe 'Posts', type: :request do
let(:user) { create(:user) }
describe 'GET /posts' do
it 'returns success' do
get posts_path
expect(response).to have_http_status(:success)
end
it 'lists posts' do
posts = create_list(:post, 3, :published)
get posts_path
expect(response.body).to include(posts.first.title)
end
end
describe 'POST /posts' do
context 'when authenticated' do
before { sign_in user }
let(:valid_params) do
{ post: attributes_for(:post) }
end
it 'creates a post' do
expect {
post posts_path, params: valid_params
}.to change(Post, :count).by(1)
end
it 'redirects to the post' do
post posts_path, params: valid_params
expect(response).to redirect_to(Post.last)
end
end
context 'when not authenticated' do
it 'redirects to sign in' do
post posts_path, params: { post: attributes_for(:post) }
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
System Specs (spec/system/)
Test complete user flows with JavaScript.
# spec/system/user_registration_spec.rb
RSpec.describe 'User Registration', type: :system do
before do
driven_by(:selenium_chrome_headless)
end
it 'allows a user to sign up' do
visit root_path
click_link 'Sign Up'
fill_in 'Email', with: 'newuser@example.com'
fill_in 'Password', with: 'password123'
fill_in 'Password confirmation', with: 'password123'
click_button 'Sign Up'
expect(page).to have_content('Welcome! You have signed up successfully.')
expect(page).to have_current_path(root_path)
end
it 'shows validation errors' do
visit new_user_registration_path
fill_in 'Email', with: 'invalid'
click_button 'Sign Up'
expect(page).to have_content('Email is invalid')
end
end
Service Specs (spec/services/)
Test service objects and business logic.
# spec/services/order_processor_spec.rb
RSpec.describe OrderProcessor do
describe '#process' do
let(:order) { create(:order, :with_items) }
subject(:processor) { described_class.new(order) }
context 'with valid order' do
it 'charges payment' do
expect(PaymentGateway).to receive(:charge).with(order)
processor.process
end
it 'updates inventory' do
expect { processor.process }.to change { order.items.first.inventory_count }
end
it 'sends confirmation' do
expect(OrderMailer).to receive(:confirmation).with(order).and_call_original
processor.process
end
it 'returns true' do
expect(processor.process).to be true
end
end
context 'with payment failure' do
before do
allow(PaymentGateway).to receive(:charge).and_raise(PaymentError, 'Card declined')
end
it 'returns false' do
expect(processor.process).to be false
end
it 'adds error to order' do
processor.process
expect(order.errors[:base]).to include('Card declined')
end
end
end
end
Testing Devise Authentication
# In request specs
RSpec.describe 'Protected pages', type: :request do
describe 'GET /dashboard' do
context 'when signed in' do
let(:user) { create(:user) }
before { sign_in user }
it 'shows dashboard' do
get dashboard_path
expect(response).to have_http_status(:success)
end
end
context 'when not signed in' do
it 'redirects to sign in' do
get dashboard_path
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
# In system specs
RSpec.describe 'User login', type: :system do
let(:user) { create(:user) }
it 'allows user to log in' do
visit new_user_session_path
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
click_button 'Log in'
expect(page).to have_content('Signed in successfully')
end
end
Running Tests
# All specs
bundle exec rspec
# Specific file
bundle exec rspec spec/models/user_spec.rb
# Specific line
bundle exec rspec spec/models/user_spec.rb:42
# By type
bundle exec rspec spec/models
bundle exec rspec spec/requests
bundle exec rspec spec/system
# With coverage
COVERAGE=true bundle exec rspec
Anti-Patterns
❌ Don't use fixtures (use FactoryBot)
❌ Don't test framework code (e.g., testing Rails validations work)
❌ Don't create tightly coupled tests
❌ Don't test multiple things in one test
❌ Don't use before(:all) (use before(:each) or let)
Navigation Guide
- FactoryBot Patterns → See
resources/factories.md - Mocking & Stubbing → See
resources/mocking.md - Test Data Strategies → See
resources/test-data.md - Coverage & CI → See
resources/coverage.md
Related Skills
- rails-dev-guidelines - For implementation patterns to test
- devise-auth-patterns - For authentication testing details
Status: Core skill (~480 lines) | 4 resource files