| name | rspec |
| description | When the user wants to design, implement, debug, or optimize RSpec tests in Ruby. Use when the user mentions "RSpec," "describe/context/it," "let," "subject," "shared_examples," "shared_context," "expect(...).to eq," "rspec-rails," "FactoryBot," "VCR," "WebMock," "Capybara," ".rspec," "rspec --tag," or "rspec spec_helper.rb." For Rails system tests see also accessibility-testing and visual-regression. For JS/TS see jest-vitest. For Python see pytest. For Java see junit-testng. |
| metadata | {"version":"1.0.0"} |
RSpec
You are an expert in RSpec — Ruby's de facto behavior-driven test framework — and the surrounding ecosystem (FactoryBot, VCR, WebMock, Capybara, rspec-rails). Your goal is to help engineers structure specs cleanly, manage test data and time, mock effectively, and integrate with Rails / Sinatra / Hanami. Don't fabricate RSpec matchers, expectation method names, or gem APIs. When uncertain, point the reader to rspec.info and the relevant gem docs.
Initial Assessment
Check .agents/qa-context.md (fallback: .claude/qa-context.md) before answering. Pay attention to:
- RSpec major version — RSpec 3.x is current. Older suites on RSpec 2 have meaningful API differences.
- Framework — Rails (use
rspec-rails), Sinatra, Hanami, pure Ruby library.
- Test data — FactoryBot (formerly factory_girl) is the de facto factory library; fixtures still appear in legacy Rails apps.
- HTTP mocking —
webmock (block real HTTP) and vcr (record-and-replay) are the standard pairing.
- Browser tests — Capybara + a driver (Selenium / Cuprite / Apparition). For non-Rails system tests, the stack is similar.
- Test scope — model / request / system specs, or pure Ruby unit specs.
If the file does not exist, ask: Ruby version, framework, RSpec version, FactoryBot or fixtures, browser-test stack if any.
Why RSpec
- Expressive describe / context / it structure — reads close to spec language.
- Rich matcher DSL —
expect(x).to eq(y), to include(...), to change { ... }.by(1).
- let / subject — lazy fixtures, scoped per example.
- Shared examples and contexts — reuse common behavior across describes.
- Tight Rails integration —
rspec-rails provides request specs, system specs, model specs.
When not to use RSpec:
- Non-Ruby code → use the host language's testing tool.
- Team prefers stdlib
Minitest for less metaprogramming overhead → Minitest is also valid in Ruby, especially in Rails ecosystem (DHH's preference).
Spec anatomy
require 'rails_helper'
RSpec.describe EmailValidator do
subject(:validator) { described_class.new }
describe '#valid?' do
it 'accepts a well-formed address' do
expect(validator.valid?('qa.user@example.com')).to be true
end
context 'with malformed input' do
%w[no-at @only double@@at.com].each do |bad|
it "rejects #{bad.inspect}" do
expect(validator.valid?(bad)).to be false
end
end
end
end
end
| Block | Use |
|---|
describe | Group examples for a class / method. |
context | Sub-group for a specific scenario / state. |
it / specify / example | One example. |
before(:each) / before(:all) | Setup hooks. |
after(:each) / after(:all) | Teardown hooks. |
let(:name) { ... } | Lazy memoized value, scoped per example. |
let!(:name) { ... } | Eager-evaluated let (runs in before). |
subject / subject(:name) | The thing under test. |
shared_examples / it_behaves_like | Reusable specs. |
shared_context / include_context | Reusable setup. |
Matchers
| Matcher | Use |
|---|
eq(x) | Equality (==). |
eql(x) | Strict equality (=== and type). |
equal(x) / be(x) | Object identity. |
be_truthy / be_falsey / be_nil | Truthy / falsey / nil. |
be_<predicate> | Matches a method ending in ? (e.g., be_valid calls valid?). |
have_attributes(name: 'x', email: ...) | Multi-attribute. |
match(/regex/) | String / array regex. |
include(x) / include(a, b) | Membership. |
contain_exactly(...) | Array equality ignoring order. |
raise_error(MyError, /message/) | Exception assertion. |
change { x }.by(1) | Side effect. |
change { x }.from(a).to(b) | Side effect with before/after. |
output(/text/).to_stdout | Captured stdout. |
Custom matchers via RSpec::Matchers.define.
let, subject, and scoping
let is lazy and memoized per example:
let(:user) { User.create!(email: 'qa.user@example.com') }
it 'has an email' do
expect(user.email).to eq('qa.user@example.com')
end
it 'has an id' do
expect(user.id).to be_present
end
let!(:user) runs in before — eager, useful when the value must exist regardless of whether the example references it (e.g., a DB row needed for a count assertion).
subject is RSpec's convention for the thing under test:
subject(:order) { build(:order, total: 1999) }
Implicit is_expected.to reads on the implicit subject:
describe Order do
subject { build(:order, total: 1999) }
it { is_expected.to be_valid }
end
Use sparingly — implicit subject can hide what's being tested.
FactoryBot
FactoryBot.define do
factory :order do
total { 1999 }
placed_at { Time.current }
trait :large do
total { 999_999 }
end
factory :paid_order do
paid_at { Time.current }
end
end
end
let(:order) { create(:order, :large, total: 5_000) }
build returns an unsaved object; create saves it; build_stubbed creates a fake-saved object without hitting the DB (much faster for tests that don't need persistence).
Avoid huge factories with many associations — each unrelated association adds DB cost.
HTTP mocking: WebMock + VCR
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
WebMock blocks real HTTP by default; tests that try to hit the network fail loudly. Stub specific requests:
stub_request(:post, 'https://billing.example.com/charge')
.with(body: hash_including(amount: 1999))
.to_return(status: 201, body: { id: 'ch_synthetic' }.to_json, headers: { 'Content-Type' => 'application/json' })
VCR records real HTTP once and replays from "cassettes" on subsequent runs:
VCR.use_cassette('billing_charge') do
process_order(order_id: 'ord-1')
end
Useful for third-party APIs. Always review committed cassettes — they contain real headers and may include secrets if the original call had them.
Time control
include ActiveSupport::Testing::TimeHelpers
it 'expires after 24 hours' do
travel_to Time.parse('2026-01-01 00:00:00 UTC') do
token = generate_token
travel 25.hours
expect(token).to be_expired
end
end
Timecop.freeze(Time.parse('2026-01-01')) do
end
Don't use sleep for time-related tests; freeze / travel time.
Rails-specific specs
rspec-rails provides:
- Model specs — pure ActiveRecord behavior tests.
- Request specs — full Rails stack via
get, post, put, patch, delete against a real router. Recommended over the older controller specs.
- System specs — Capybara-backed browser tests for end-to-end UI behavior.
- Mailer / Job / Channel specs — domain-specific.
RSpec.describe 'POST /checkout', type: :request do
let(:headers) { { 'Authorization' => 'Bearer bearer-token-placeholder' } }
it 'returns 201' do
post '/checkout', params: { sku: 'sku-001', qty: 1 }.to_json,
headers: headers.merge('Content-Type' => 'application/json')
expect(response).to have_http_status(:created)
expect(response.parsed_body).to include('order_id')
end
end
Configuration
RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
c.max_formatted_output_length = 1000
end
config.example_status_persistence_file_path = 'tmp/rspec_examples.txt'
config.order = :random
config.profile_examples = 10
config.filter_run_when_matching :focus
end
Running
| Command | Purpose |
|---|
rspec | Run all specs. |
rspec spec/models/order_spec.rb | One file. |
rspec spec/models/order_spec.rb:42 | Spec at line 42. |
rspec --tag smoke | Tagged with :smoke. |
rspec --tag ~slow | Excluding :slow. |
rspec --only-failures | Re-run last failures. |
rspec --next-failure | One failure at a time. |
rspec --format documentation | Verbose. |
rspec --format JUnit --out report.xml (via rspec_junit_formatter) | CI integration. |
Verify flags with rspec --help against your installed version.
Parallel runs
The Ruby community uses two main approaches:
parallel_tests — splits the suite across multiple processes locally.
- CI sharding — multiple workers each running a subset (using
parallel_tests or rspec-queue).
In Rails, parallel_tests needs DB strategies that isolate per worker (parallel_tests:db:setup creates myapp_test1, myapp_test2, etc.).
Common Pitfalls
should syntax — deprecated. Always use expect(...).to ....
- Heavy
before(:all) — state leaks across examples. Default to before(:each) and let.
let! everywhere — defeats laziness. Use only when needed (counts, ordering).
- Implicit
subject with complex setups — hides what's being tested. Name the subject.
- Hitting the network unintentionally — always
WebMock.disable_net_connect!(allow_localhost: true).
- Committing VCR cassettes with real secrets — scrub before commit; configure VCR's
filter_sensitive_data.
- Order-dependent specs —
config.order = :random surfaces these; fix them, don't disable random order.
- Massive factory associations — every
let(:order) creates a cascade of dependencies. Use build_stubbed where DB isn't needed.
- Mixing controller specs and request specs — controller specs are deprecated; migrate.
sleep in specs — replaces with proper time/HTTP mocking or polling.
Task-Specific Questions
When helping with RSpec, ask:
- Ruby version and RSpec version?
- Rails, Sinatra, Hanami, or pure Ruby?
- FactoryBot or fixtures?
- Browser tests in scope (Capybara + which driver)?
- HTTP mocking strategy — WebMock, VCR, both?
- Parallel test runs —
parallel_tests, rspec-queue, or single-process?
- CI reporter — JUnit, RSpec docs format, Allure?
Related Skills
- jest-vitest — JS/TS equivalent.
- pytest — Python equivalent.
- junit-testng — JVM equivalent.
- xunit-nunit — .NET equivalent.
- go-test — Go equivalent.
- cypress / playwright — when RSpec system specs grow expensive, consider moving E2E to a JS-based runner.
- mutation-testing —
mutant is the Ruby tool.
- code-coverage — SimpleCov.
- flaky-test-management — when order randomization surfaces dependencies.
- ci-test-orchestration — for
parallel_tests and matrix sharding.