RSpec Testing Skill
Expert guidance for writing comprehensive tests in RSpec for Ruby and Rails applications. This skill provides immediate, actionable testing strategies with deep-dive references for complex scenarios.
Quick Start
Basic RSpec Structure
spec/models/user_spec.rb
RSpec.describe User, type: :model do describe '#full_name' do it 'returns the first and last name' do user = User.new(first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end end end
Key concepts:
-
describe : Groups related tests (classes, methods)
-
context : Describes specific scenarios
-
it : Individual test example
-
expect : Makes assertions using matchers
Running Tests
Run all specs
bundle exec rspec
Run specific file
bundle exec rspec spec/models/user_spec.rb
Run specific line
bundle exec rspec spec/models/user_spec.rb:12
Run with documentation format
bundle exec rspec --format documentation
Run only failures from last run
bundle exec rspec --only-failures
Core Testing Patterns
- Model Specs
Test business logic, validations, associations, and methods:
RSpec.describe Article, type: :model do
Test validations
describe 'validations' do it { should validate_presence_of(:title) } it { should validate_length_of(:title).is_at_most(100) } end
Test associations
describe 'associations' do it { should belong_to(:author) } it { should have_many(:comments) } end
Test instance methods
describe '#published?' do context 'when publish_date is in the past' do it 'returns true' do article = Article.new(publish_date: 1.day.ago) expect(article.published?).to be true end end
context 'when publish_date is in the future' do
it 'returns false' do
article = Article.new(publish_date: 1.day.from_now)
expect(article.published?).to be false
end
end
end
Test scopes
describe '.recent' do it 'returns articles from the last 30 days' do old = create(:article, created_at: 31.days.ago) recent = create(:article, created_at: 1.day.ago)
expect(Article.recent).to include(recent)
expect(Article.recent).not_to include(old)
end
end end
- Request Specs
Test HTTP requests and responses across the entire stack:
RSpec.describe 'Articles API', type: :request do describe 'GET /articles' do it 'returns all articles' do create_list(:article, 3)
get '/articles'
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).size).to eq(3)
end
end
describe 'POST /articles' do context 'with valid params' do it 'creates a new article' do article_params = { article: { title: 'New Article', body: 'Content' } }
expect {
post '/articles', params: article_params
}.to change(Article, :count).by(1)
expect(response).to have_http_status(:created)
end
end
context 'with invalid params' do
it 'returns errors' do
invalid_params = { article: { title: '' } }
post '/articles', params: invalid_params
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe 'authentication' do it 'requires authentication for create' do post '/articles', params: { article: { title: 'Test' } }
expect(response).to have_http_status(:unauthorized)
end
it 'allows authenticated users to create' do
user = create(:user)
post '/articles',
params: { article: { title: 'Test' } },
headers: { 'Authorization' => "Bearer #{user.token}" }
expect(response).to have_http_status(:created)
end
end end
- System Specs (End-to-End)
Test user workflows through the browser with Capybara:
RSpec.describe 'Article management', type: :system do before { driven_by(:selenium_chrome_headless) }
scenario 'user creates an article' do visit new_article_path
fill_in 'Title', with: 'My Article'
fill_in 'Body', with: 'Article content'
click_button 'Create Article'
expect(page).to have_content('Article was successfully created')
expect(page).to have_content('My Article')
end
scenario 'user edits an article' do article = create(:article, title: 'Original Title')
visit article_path(article)
click_link 'Edit'
fill_in 'Title', with: 'Updated Title'
click_button 'Update Article'
expect(page).to have_content('Updated Title')
expect(page).not_to have_content('Original Title')
end
Test JavaScript interactions
scenario 'user filters articles', js: true do create(:article, title: 'Ruby Article', category: 'ruby') create(:article, title: 'Python Article', category: 'python')
visit articles_path
select 'Ruby', from: 'filter'
expect(page).to have_content('Ruby Article')
expect(page).not_to have_content('Python Article')
end end
Factory Bot Integration
Use FactoryBot for test data. Define factories in spec/factories/ , use traits for variations, and prefer build over create when persistence isn't needed.
See references/factory_bot.md for factory definitions, traits, sequences, associations, and build strategies.
Essential Matchers
Category Examples
Equality eq , eql , be , equal
Truthiness be_truthy , be_falsy , be_nil , be_a
Collections include , contain_exactly , match_array
Changes change , raise_error , have_enqueued_job
See references/matchers.md for the complete matcher reference.
Mocks, Stubs, and Doubles
Use double or instance_double (preferred — verifies against real class) for test doubles. Stub with allow(obj).to receive(:method) , set expectations with expect(obj).to receive(:method) , and verify after the fact with spies via have_received .
See references/mocking.md for test doubles, stubbing, message expectations, and spies.
DRY Testing Techniques
Use let (lazy) and let! (eager) for test data, before hooks for shared setup, shared_examples for reusable test groups, and shared_context for reusable setup blocks. Prefer subject for the object under test.
See references/core_concepts.md for detailed coverage of hooks, let, shared examples, shared contexts, and subject.
TDD Workflow
Red-Green-Refactor Cycle
- Red: Write a failing test first
describe User do it 'has a full name' do user = User.new(first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end end
Fails: undefined method `full_name'
- Green: Write minimal code to pass
class User def full_name "#{first_name} #{last_name}" end end
Passes!
- Refactor: Improve code while keeping tests green
Testing Strategy
Start with system specs for user-facing features:
-
Tests complete workflows
-
Highest confidence
-
Slowest to run
Drop to request specs for API/controller logic:
-
Test HTTP interactions
-
Faster than system specs
-
Cover authentication, authorization, edge cases
Use model specs for business logic:
-
Test calculations, validations, scopes
-
Fast and focused
-
Most of your test suite
Configuration Best Practices
spec/rails_helper.rb
require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' abort("Run in production!") if Rails.env.production? require 'rspec/rails'
Auto-require support files
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
RSpec.configure do |config|
Use transactional fixtures
config.use_transactional_fixtures = true
Infer spec type from file location
config.infer_spec_type_from_file_location!
Filter Rails backtrace
config.filter_rails_from_backtrace!
Include FactoryBot methods
config.include FactoryBot::Syntax::Methods
Include request helpers
config.include RequestHelpers, type: :request
Capybara configuration for system specs
config.before(:each, type: :system) do driven_by :selenium_chrome_headless end end
spec/spec_helper.rb
RSpec.configure do |config|
Show detailed failure messages
config.example_status_persistence_file_path = "spec/examples.txt"
Disable monkey patching (use expect syntax only)
config.disable_monkey_patching!
Output warnings
config.warnings = true
Profile slowest tests
config.profile_examples = 10 if ENV['PROFILE']
Run specs in random order
config.order = :random Kernel.srand config.seed end
Common Patterns
For testing background jobs, mailers, file uploads, pagination, and search, see references/rails_testing.md.
Performance Tips
Use let instead of before for lazy loading
Avoid database calls when testing logic (use mocks)
Use build instead of create when persistence isn't needed
Use build_stubbed for non-persisted objects with associations
Tag slow tests and exclude them during development:
it 'slow test', :slow do
test code
end
Run with: rspec --tag ~slow
When to Use Each Spec Type
-
Model specs: Business logic, calculations, validations, scopes
-
Request specs: API endpoints, authentication, authorization, JSON responses
-
System specs: User workflows, JavaScript interactions, form submissions
-
Mailer specs: Email content, recipients, attachments
-
Job specs: Background job enqueueing and execution
-
Helper specs: View helper methods
-
Routing specs: Custom routes (usually not needed)
Quick Reference
Most Common Commands:
rspec # Run all specs rspec spec/models # Run model specs rspec --tag ~slow # Exclude slow specs rspec --only-failures # Rerun failures rspec --format documentation # Readable output rspec --profile # Show slowest specs
Most Common Matchers:
-
eq(expected)
-
value equality
-
be_truthy / be_falsy
-
truthiness
-
include(item)
-
collection membership
-
raise_error(Error)
-
exceptions
-
change { }.by(n)
-
state changes
Most Common Stubs:
-
allow(obj).to receive(:method)
-
stub method
-
expect(obj).to receive(:method)
-
expect call
-
double('name', method: value)
-
create double
Reference Documentation
For detailed information on specific topics, see the references directory:
-
Core Concepts - Describe blocks, contexts, hooks, subject, let
-
Matchers Guide - Complete matcher reference with examples
-
Mocking and Stubbing - Test doubles, stubs, spies, message expectations
-
Rails Testing - Rails-specific spec types and helpers
-
Factory Bot - Test data strategies and patterns
-
Best Practices - Testing philosophy, patterns, and anti-patterns
-
Configuration - Setup, formatters, and optimization
Common Scenarios
For debugging failing tests, testing complex queries, and testing callbacks, see references/rails_testing.md.