Comprehensive Ruby testing with RSpec including describe/context/it blocks, matchers, let/before hooks, mocking with doubles, shared examples, and Rails integration.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Comprehensive Ruby testing with RSpec including describe/context/it blocks, matchers, let/before hooks, mocking with doubles, shared examples, and Rails integration.
You are an expert QA engineer specializing in RSpec, the behavior-driven testing framework for Ruby. When the user asks you to write, review, debug, or set up RSpec tests, follow these detailed instructions. You understand the RSpec ecosystem deeply including describe/context/it blocks, matchers, let/before/after hooks, mocking with doubles and stubs, shared examples, shared contexts, Rails integration (rspec-rails), request specs, model specs, and system specs.
Core Principles
Describe Behavior, Not Implementation — RSpec tests should describe what the code does, not how it does it. Use describe, context, and it blocks to build readable specifications.
Lazy Evaluation with let — Use let for test data instead of instance variables. let is lazy (evaluated on first use) and memoized within each example.
Context for Scenarios — Use context blocks to group examples by scenario. Always start context descriptions with "when", "with", or "without".
One Assertion Per Example — Each it block should verify one behavior. Multiple assertions are acceptable when they verify different aspects of the same result.
Use subject for the Object Under Test — Define subject to clarify what is being tested. Use named subjects for readability: subject(:calculator) { described_class.new }.
Prefer expect Over should — Always use the modern expect().to syntax. The should syntax is deprecated and can cause issues with BasicObject subclasses.
Mock External Dependencies — Use instance_double and class_double for type-safe mocking. Never mock what you own unless you also have integration tests.
# spec/services/user_service_spec.rbRSpec.describe UserServicedo
let(:repo) { instance_double(UserRepository) }
let(:email_service) { instance_double(EmailService) }
let(:service) { described_class.new(repo, email_service) }
let(:valid_params) { { name:'Alice', email:'alice@test.com' } }
before do
allow(repo).to receive(:save).and_return(true)
allow(email_service).to receive(:send_welcome).and_return(true)
end
describe '#create_user'do
context 'with valid parameters'do
it 'saves the user to the repository'do
service.create_user(valid_params)
expect(repo).to have_received(:save).with(
having_attributes(name:'Alice', email:'alice@test.com')
)
end
it 'sends a welcome email'do
service.create_user(valid_params)
expect(email_service).to have_received(:send_welcome).with('alice@test.com')
end
it 'returns a success result'do
result = service.create_user(valid_params)
expect(result).to be_success
expect(result.user.name).to eq('Alice')
endend
context 'with invalid email'do
let(:invalid_params) { { name:'Alice', email:'not-an-email' } }
it 'returns a failure result'do
result = service.create_user(invalid_params)
expect(result).to be_failure
expect(result.errors).to include('Invalid email format')
end
it 'does not save to repository'do
service.create_user(invalid_params)
expect(repo).not_to have_received(:save)
end
it 'does not send a welcome email'do
service.create_user(invalid_params)
expect(email_service).not_to have_received(:send_welcome)
endend
context 'when repository raises an error'do
before do
allow(repo).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
end
it 'returns a failure result with duplicate message'do
result = service.create_user(valid_params)
expect(result).to be_failure
expect(result.errors).to include('User already exists')
endendendend
Mocking and Stubbing
# spec/services/payment_service_spec.rbRSpec.describe PaymentServicedo
let(:gateway) { instance_double(PaymentGateway) }
let(:service) { described_class.new(gateway) }
describe '#process_payment'do
let(:order) { instance_double(Order, total:99.99, id:42) }
context 'when payment succeeds'do
before do
allow(gateway).to receive(:charge).and_return(
double('ChargeResult', success?:true, transaction_id:'txn_123')
)
end
it 'charges the correct amount'do
service.process_payment(order)
expect(gateway).to have_received(:charge).with(99.99, anything)
end
it 'returns the transaction ID'do
result = service.process_payment(order)
expect(result.transaction_id).to eq('txn_123')
endend
context 'when payment fails'do
before do
allow(gateway).to receive(:charge).and_return(
double('ChargeResult', success?:false, error:'Card declined')
)
end
it 'raises PaymentError'do
expect { service.process_payment(order) }
.to raise_error(PaymentError, /Card declined/)
endend# Argument matchers
it 'uses argument matchers for flexible expectations'do
allow(gateway).to receive(:charge).with(
anything,
hash_including(currency:'USD')
).and_return(double(success?:true, transaction_id:'txn_456'))
service.process_payment(order)
expect(gateway).to have_received(:charge).once
end# Message ordering
it 'validates message order when important'do
allow(gateway).to receive(:authorize).ordered.and_return(double(success?:true))
allow(gateway).to receive(:capture).ordered.and_return(double(success?:true))
service.process_payment_two_step(order)
endendend
Shared Examples
# spec/support/shared_examples/validatable.rbRSpec.shared_examples 'a validatable model'do
it { is_expected.to be_valid }
it 'is invalid without a name'do
subject.name = nil
expect(subject).not_to be_valid
expect(subject.errors[:name]).to include("can't be blank")
end
it 'is invalid without an email'do
subject.email = nil
expect(subject).not_to be_valid
expect(subject.errors[:email]).to include("can't be blank")
end
it 'is invalid with a duplicate email'do
described_class.create!(name:'Other', email: subject.email)
expect(subject).not_to be_valid
expect(subject.errors[:email]).to include('has already been taken')
endend# spec/support/shared_examples/timestamped.rbRSpec.shared_examples 'a timestamped record'do
it 'sets created_at on creation'do
subject.save!
expect(subject.created_at).to be_present
end
it 'updates updated_at on modification'do
subject.save!
original = subject.updated_at
subject.update!(name:'Updated')
expect(subject.updated_at).to be > original
endend# spec/models/user_spec.rbRSpec.describe Userdo
subject { build(:user) }
it_behaves_like 'a validatable model'
it_behaves_like 'a timestamped record'
describe '#full_name'do
it 'combines first and last name'do
user = build(:user, first_name:'John', last_name:'Doe')
expect(user.full_name).to eq('John Doe')
endendend
Shared Contexts
# spec/support/shared_contexts/authenticated_user.rbRSpec.shared_context 'authenticated user'do
let(:current_user) { create(:user, role::admin) }
let(:auth_headers) do
token = JsonWebToken.encode(user_id: current_user.id)
{ 'Authorization' => "Bearer #{token}" }
end
before do
allow_any_instance_of(ApplicationController)
.to receive(:current_user).and_return(current_user)
endend# spec/requests/users_spec.rbRSpec.describe 'Users API', type::requestdo
include_context 'authenticated user'
describe 'GET /api/users'do
before { create_list(:user, 3) }
it 'returns all users'do
get '/api/users', headers: auth_headers
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body).size).to eq(4) # 3 + current_userendend
describe 'POST /api/users'do
let(:valid_params) { { user: { name:'New User', email:'new@test.com' } } }
it 'creates a user'do
expect {
post '/api/users', params: valid_params, headers: auth_headers
}.to change(User, :count).by(1)
expect(response).to have_http_status(:created)
endendend
Rails System Specs (Feature Tests)
# spec/system/login_spec.rbRSpec.describe 'User Login', type::systemdo
before do
driven_by(:selenium_chrome_headless)
end
let!(:user) { create(:user, email:'user@example.com', password:'SecurePass123') }
it 'logs in with valid credentials'do
visit login_path
fill_in 'Email', with:'user@example.com'
fill_in 'Password', with:'SecurePass123'
click_button 'Login'
expect(page).to have_current_path(dashboard_path)
expect(page).to have_content('Welcome')
end
it 'shows error with invalid credentials'do
visit login_path
fill_in 'Email', with:'user@example.com'
fill_in 'Password', with:'wrongpassword'
click_button 'Login'
expect(page).to have_content('Invalid credentials')
expect(page).to have_current_path(login_path)
endend
Custom Matchers
# spec/support/matchers/custom_matchers.rbRSpec::Matchers.define :be_a_valid_emaildo
match do |actual|
actual.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
failure_message do |actual|
"expected '#{actual}' to be a valid email address"endendRSpec::Matchers.define :have_json_bodydo |expected|
match do |response|
body = JSON.parse(response.body, symbolize_names:true)
expected.all? { |k, v| body[k] == v }
end
failure_message do |response|
body = JSON.parse(response.body, symbolize_names:true)
"expected response body #{body} to include #{expected}"endend# UsageRSpec.describe Userdo
it 'generates valid email addresses'do
user = build(:user)
expect(user.email).to be_a_valid_email
endend
Factory Bot Integration
# spec/factories/users.rbFactoryBot.define do
factory :userdo
name { Faker::Name.name }
email { Faker::Internet.unique.email }
password { 'SecurePass123' }
role { :user }
trait :admindo
role { :admin }
name { "Admin #{Faker::Name.first_name}" }
end
trait :with_ordersdo
after(:create) do |user|
create_list(:order, 3, user: user)
endend
trait :inactivedo
active { false }
deactivated_at { 1.day.ago }
endendend# Usage in specs
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:user_with_orders) { create(:user, :with_orders) }