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.
Provides comprehensive RSpec testing guidance for Ruby/Rails apps. Automatically triggers on RSpec-related keywords and testing scenarios, offering patterns for model specs, request specs, system specs, factories, mocks, and TDD workflow.
/plugin marketplace add el-feo/ai-context/plugin install ruby-rails@jebs-dev-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/best_practices.mdreferences/configuration.mdreferences/core_concepts.mdreferences/factory_bot.mdreferences/matchers.mdreferences/mocking.mdreferences/rails_testing.mdExpert 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.
# 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# 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
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
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
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
# 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
expect(actual).to eq(expected) # ==
expect(actual).to eql(expected) # .eql?
expect(actual).to be(expected) # .equal?
expect(actual).to equal(expected) # same object
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)
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)
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')
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)
# Basic double
book = double('book', title: 'RSpec Book', pages: 300)
# Verifying double (checks against real class)
book = instance_double('Book', title: 'RSpec Book')
# 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])
# 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)
# 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)
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
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
# 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
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
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'
class User
def full_name
"#{first_name} #{last_name}"
end
end
# Passes!
Start with system specs for user-facing features:
Drop to request specs for API/controller logic:
Use model specs for business logic:
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
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
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
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
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
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
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 changesMost Common Stubs:
allow(obj).to receive(:method) - stub methodexpect(obj).to receive(:method) - expect calldouble('name', method: value) - create doubleFor detailed information on specific topics, see the references directory:
# 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
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
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.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.