| name | rspec |
| description | Comprehensive RSpec testing for Ruby and Rails applications. Covers model specs, request specs, system specs, factories, mocks, and TDD workflow. Automatically triggers on RSpec-related keywords and testing scenarios. |
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 scenariosit: Individual test exampleexpect: 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
1. 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
2. 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
3. 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
Defining Factories
# spec/factories/users.rb
FactoryBot.define do
factory :user do
first_name { 'John' }
last_name { 'Doe' }
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password123' }
# Traits for variations
trait :admin do
role { 'admin' }
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
end
factory :article do
sequence(:title) { |n| "Article #{n}" }
body { 'Article content' }
association :author, factory: :user
end
end
# Using factories
user = create(:user) # Persisted
user = build(:user) # Not persisted
admin = create(:user, :admin) # With trait
user = create(:user, :with_articles) # With association
users = create_list(:user, 5) # Multiple records
attributes = attributes_for(:user) # Hash of attributes
Essential Matchers
Equality and Identity
expect(actual).to eq(expected) # ==
expect(actual).to eql(expected) # .eql?
expect(actual).to be(expected) # .equal?
expect(actual).to equal(expected) # same object
Truthiness and Types
expect(actual).to be_truthy # not nil or false
expect(actual).to be_falsy # nil or false
expect(actual).to be_nil
expect(actual).to be_a(Class)
expect(actual).to be_an_instance_of(Class)
Collections
expect(array).to include(item)
expect(array).to contain_exactly(1, 2, 3) # any order
expect(array).to match_array([1, 2, 3]) # any order
expect(array).to start_with(1, 2)
expect(array).to end_with(2, 3)
Errors and Changes
expect { action }.to raise_error(ErrorClass)
expect { action }.to raise_error('message')
expect { action }.to change(User, :count).by(1)
expect { action }.to change { user.reload.name }.from('old').to('new')
Rails-Specific
expect(response).to have_http_status(:success)
expect(response).to have_http_status(200)
expect(response).to redirect_to(path)
expect { action }.to have_enqueued_job(JobClass)
Mocks, Stubs, and Doubles
Test Doubles
# Basic double
book = double('book', title: 'RSpec Book', pages: 300)
# Verifying double (checks against real class)
book = instance_double('Book', title: 'RSpec Book')
Stubbing Methods
# On test doubles
allow(book).to receive(:title).and_return('New Title')
allow(book).to receive(:available?).and_return(true)
# On real objects
user = User.new
allow(user).to receive(:admin?).and_return(true)
# Chaining
allow(user).to receive_message_chain(:articles, :published).and_return([article])
Message Expectations
# Expect method to be called
expect(mailer).to receive(:deliver).and_return(true)
# With specific arguments
expect(service).to receive(:call).with(user, { notify: true })
# Number of times
expect(logger).to receive(:info).once
expect(logger).to receive(:info).twice
expect(logger).to receive(:info).exactly(3).times
expect(logger).to receive(:info).at_least(:once)
Spies
# Create spy
invitation = spy('invitation')
user.accept_invitation(invitation)
# Verify after the fact
expect(invitation).to have_received(:accept)
expect(invitation).to have_received(:accept).with(mailer)
DRY Testing Techniques
Before Hooks
RSpec.describe ArticlesController do
before(:each) do
@user = create(:user)
sign_in @user
end
# OR using subject
subject { create(:article) }
it 'has a title' do
expect(subject.title).to be_present
end
end
Let and Let
describe Article do
let(:article) { create(:article) } # Lazy-loaded
let!(:published) { create(:article, :published) } # Eager-loaded
it 'can access article' do
expect(article).to be_valid
end
end
Shared Examples
# Define shared examples
RSpec.shared_examples 'a timestamped model' do
it 'has created_at' do
expect(subject).to respond_to(:created_at)
end
it 'has updated_at' do
expect(subject).to respond_to(:updated_at)
end
end
# Use shared examples
describe Article do
it_behaves_like 'a timestamped model'
end
describe Comment do
it_behaves_like 'a timestamped model'
end
Shared Contexts
RSpec.shared_context 'authenticated user' do
let(:current_user) { create(:user) }
before do
sign_in current_user
end
end
describe ArticlesController do
include_context 'authenticated user'
# Tests use current_user and are signed in
end
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
Testing Background Jobs
describe 'background jobs', type: :job do
it 'enqueues the job' do
expect {
SendEmailJob.perform_later(user)
}.to have_enqueued_job(SendEmailJob).with(user)
end
it 'performs the job' do
expect {
SendEmailJob.perform_now(user)
}.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
Testing Mailers
describe UserMailer, type: :mailer do
describe '#welcome_email' do
let(:user) { create(:user) }
let(:mail) { UserMailer.welcome_email(user) }
it 'renders the subject' do
expect(mail.subject).to eq('Welcome!')
end
it 'renders the receiver email' do
expect(mail.to).to eq([user.email])
end
it 'renders the sender email' do
expect(mail.from).to eq(['noreply@example.com'])
end
it 'contains the user name' do
expect(mail.body.encoded).to include(user.name)
end
end
end
Testing File Uploads
describe 'file upload', type: :system do
it 'allows user to upload avatar' do
user = create(:user)
sign_in user
visit edit_profile_path
attach_file 'Avatar', Rails.root.join('spec', 'fixtures', 'avatar.jpg')
click_button 'Update Profile'
expect(page).to have_content('Profile updated')
expect(user.reload.avatar).to be_attached
end
end
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 equalitybe_truthy/be_falsy- truthinessinclude(item)- collection membershipraise_error(Error)- exceptionschange { }.by(n)- state changes
Most Common Stubs:
allow(obj).to receive(:method)- stub methodexpect(obj).to receive(:method)- expect calldouble('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
Debugging Failing Tests
# Use save_and_open_page in system specs
scenario 'user creates article' do
visit new_article_path
save_and_open_page # Opens browser with current page state
# ...
end
# Print response body in request specs
it 'creates article' do
post '/articles', params: { ... }
puts response.body # Debug API responses
expect(response).to be_successful
end
# Use binding.pry for interactive debugging
it 'calculates total' do
order = create(:order)
binding.pry # Pause execution here
expect(order.total).to eq(100)
end
Testing Complex Queries
describe '.search' do
let!(:ruby_article) { create(:article, title: 'Ruby Guide', body: 'Ruby content') }
let!(:rails_article) { create(:article, title: 'Rails Guide', body: 'Rails content') }
it 'finds articles by title' do
results = Article.search('Ruby')
expect(results).to include(ruby_article)
expect(results).not_to include(rails_article)
end
it 'finds articles by body' do
results = Article.search('Rails content')
expect(results).to include(rails_article)
end
end
Testing Callbacks
describe 'callbacks' do
describe 'after_create' do
it 'sends welcome email' do
expect(UserMailer).to receive(:welcome_email)
.with(an_instance_of(User))
.and_return(double(deliver_later: true))
create(:user)
end
end
describe 'before_save' do
it 'normalizes email' do
user = create(:user, email: 'USER@EXAMPLE.COM')
expect(user.email).to eq('user@example.com')
end
end
end
This skill provides comprehensive RSpec testing guidance. For specific scenarios or advanced techniques, refer to the detailed reference documentation in the references/ directory.