| 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