home / skills / el-feo / ai-context / rspec
npx playbooks add skill el-feo/ai-context --skill rspecReview the files below or copy the command above to add this skill to your agents.
---
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
```ruby
# 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
```bash
# 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:
```ruby
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:
```ruby
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:
```ruby
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
```ruby
# 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
```ruby
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
```ruby
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
```ruby
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
```ruby
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
```ruby
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
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
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
```ruby
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
```ruby
# 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
```ruby
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
1. **Red**: Write a failing test first
```ruby
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'
```
2. **Green**: Write minimal code to pass
```ruby
class User
def full_name
"#{first_name} #{last_name}"
end
end
# Passes!
```
3. **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
```ruby
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
```ruby
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
```ruby
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
```ruby
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(['[email protected]'])
end
it 'contains the user name' do
expect(mail.body.encoded).to include(user.name)
end
end
end
```
### Testing File Uploads
```ruby
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
1. **Use let instead of before** for lazy loading
2. **Avoid database calls** when testing logic (use mocks)
3. **Use build instead of create** when persistence isn't needed
4. **Use build_stubbed** for non-persisted objects with associations
5. **Tag slow tests** and exclude them during development:
```ruby
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:**
```bash
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](./references/core_concepts.md)** - Describe blocks, contexts, hooks, subject, let
- **[Matchers Guide](./references/matchers.md)** - Complete matcher reference with examples
- **[Mocking and Stubbing](./references/mocking.md)** - Test doubles, stubs, spies, message expectations
- **[Rails Testing](./references/rails_testing.md)** - Rails-specific spec types and helpers
- **[Factory Bot](./references/factory_bot.md)** - Test data strategies and patterns
- **[Best Practices](./references/best_practices.md)** - Testing philosophy, patterns, and anti-patterns
- **[Configuration](./references/configuration.md)** - Setup, formatters, and optimization
## Common Scenarios
### Debugging Failing Tests
```ruby
# 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
```ruby
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
```ruby
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: '[email protected]')
expect(user.email).to eq('[email protected]')
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.