rspec · v1.0.0 · 2026-05-16 · sha256 c050759fc4027a24

rspec v1.0.0A

Immutable. This exact content is served forever at /api/v1/blob/c050759fc4027a24.

---
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

```ruby
# spec/models/email_validator_spec.rb
require 'rails_helper'  # or 'spec_helper' for pure Ruby

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:

```ruby
let(:user) { User.create!(email: 'qa.user@example.com') }

it 'has an email' do
  expect(user.email).to eq('qa.user@example.com')  # creates here
end

it 'has an id' do
  expect(user.id).to be_present  # creates anew in this example
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:

```ruby
subject(:order) { build(:order, total: 1999) }
```

Implicit `is_expected.to` reads on the implicit subject:

```ruby
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

```ruby
# spec/factories/orders.rb
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

# in a spec
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

```ruby
# spec/support/webmock.rb
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:

```ruby
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:

```ruby
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

```ruby
# Using ActiveSupport::Testing::TimeHelpers (Rails)
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

# Using Timecop (gem)
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.

```ruby
# Request spec
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

```ruby
# spec/spec_helper.rb (pure ruby) or spec/rails_helper.rb (Rails)
RSpec.configure do |config|
  config.expect_with :rspec do |c|
    c.syntax = :expect  # only :expect (not :should)
    c.max_formatted_output_length = 1000
  end

  config.example_status_persistence_file_path = 'tmp/rspec_examples.txt'  # for --next-failure / --only-failures
  config.order = :random
  config.profile_examples = 10   # log slowest 10
  config.filter_run_when_matching :focus  # use `it :focus do` for ad-hoc 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:

1. Ruby version and RSpec version?
2. Rails, Sinatra, Hanami, or pure Ruby?
3. FactoryBot or fixtures?
4. Browser tests in scope (Capybara + which driver)?
5. HTTP mocking strategy — WebMock, VCR, both?
6. Parallel test runs — `parallel_tests`, `rspec-queue`, or single-process?
7. 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.