rspec
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.
$ Installer
git clone https://github.com/el-feo/ai-context /tmp/ai-context && cp -r /tmp/ai-context/plugins/ruby-rails/skills/rspec ~/.claude/skills/ai-context// tip: Run this command in your terminal to install the skill
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.
Repository
