RSpec testing patterns, factories, mocks, and test best practices
Ensure comprehensive, maintainable test coverage using RSpec, FactoryBot, and supporting gems configured for the application.
The app uses:
When adding a new feature:
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
# 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
❌ 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
# 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
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
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
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
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
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
# 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
# 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
❌ 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)
resources/factories.mdresources/mocking.mdresources/test-data.mdresources/coverage.mdStatus: Core skill (~480 lines) | 4 resource files
npx skills add ThibautBaissac/rspec-testing-guidelines下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer