rspec-best-practices/SKILL.md
Use when writing, reviewing, or cleaning up RSpec tests for Ruby and Rails codebases. Covers spec type selection, factory design, flaky test fixes, shared examples, deterministic assertions, test-driven development discipline, and choosing the best first failing spec for Rails changes. Also applies when choosing between model, request, system, and job specs.
npx skillsauth add igmarin/rails-agent-skills rspec-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when the task is to write, review, or clean up RSpec tests.
Core principle: Prefer behavioral confidence over implementation coupling. Good specs are readable, deterministic, and cheap to maintain.
| Aspect | Rule |
|--------|------|
| Spec types | Model: domain logic; Request: HTTP endpoints (prefer over controller); Job: background processing; Service/PORO: no Rails helpers; System: critical E2E only (slow) |
| Assertions | Test behavior, not implementation |
| Factories | Minimal — only attributes needed; use traits for optional states; prefer build/build_stubbed over create |
| Mocking | Stub external boundaries, not internal code |
| Isolation | Each example independent; no shared mutable state |
| Naming | describe for class/method, context for scenario |
| Service specs | Required: describe '.call' and subject(:result) for the primary invocation |
| let vs let! | Default to let; let! ONLY when object must exist before example runs |
| External service mocking | allow(ServiceClass).to receive(:method) — not instance_double; instance_double only for injected collaborators |
| Example names | Present tense: it 'returns the user', never it 'should ...'; NEVER contains the word "and" — split into separate examples |
| aggregate_failures | Use when asserting multiple related items in one example |
When driving new behaviour with RSpec, follow this sequence:
| Change type | Start with | |-------------|------------| | Pure domain logic | Model or PORO service spec | | HTTP endpoint behaviour | Request spec | | Background processing | Job spec | | Cross-layer user journey | System spec (sparingly) |
Minimal factories only. Never rely on factory defaults for business logic — set explicitly or use traits. Avoid create when build/build_stubbed suffices.
RSpec.describe Invoices::MarkOverdue do
describe '.call' do
subject(:result) { described_class.call(invoice: invoice) }
context 'when the invoice is overdue and unpaid' do
let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: nil) }
it 'marks the invoice overdue' do
expect { result }.to change { invoice.reload.overdue? }.from(false).to(true)
end
end
context 'when the invoice is already paid' do
let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: 1.day.ago) }
it 'does not change the invoice' do
expect { result }.not_to change { invoice.reload.updated_at }
end
end
end
end
→ Full examples: EXAMPLES.md | Copy-paste templates: assets/spec_templates.md
Use only when the same behavioural contract applies to multiple subjects without per-example let overrides. Avoid when each context needs different setup — that signals a wrong abstraction. → Example in EXAMPLES.md
The word "and" in an it / specify description means the example is asserting two behaviors. Split it. One behavior per example. Applies to every spec type — model, request, service, job, mailer, system.
# BAD — two assertions; if the first fails, the second never runs
it 'returns 201 and creates the record' do; end
it 'saves the order and sends the confirmation email' do; end
it 'updates the user and logs the change' do; end
# GOOD — one observable outcome per example
it 'returns 201' do; end
it 'creates the record' do; end
it 'saves the order' do; end
it 'sends the confirmation email' do; end
Self-check before finalizing any spec: scan every it '...' / it "..." / specify '...' string for the word and (case-insensitive, word-boundary). Every hit is a split — no exceptions for "convenience" examples like 'returns nil and does not raise'.
When asked to write or review RSpec specs, your output MUST satisfy each rule below. Each is graded independently — one violation drops the whole check.
app/foo/bar.rb → spec/foo/bar_spec.rb.# frozen_string_literal: true as the first line of every spec file.RSpec.describe uses the full constant path (RSpec.describe Module::Class do), not a string.describe '#method' / describe '.class_method' for each method under test.context 'when ...' / context 'with ...' for scenario variations — never use context to group methods.let for test data, let! ONLY when the object must exist before the action under test.let_it_be unless the project already depends on test-prof (check Gemfile.lock first).subject(:result) { ... } for service / PORO specs invoking .call.travel_to / freeze_time for any time-dependent assertion — never set past Time.now or stub Time.current directly.allow(SomeClient).to receive(:method)); ActiveRecord finders are NEVER mocked.| Cause | Fix |
|-------|-----|
| Time-dependent logic | freeze_time / travel_to; never set past dates as shortcut |
| State leakage | Each example sets up own state; avoid before(:all) |
| Async jobs | queue_adapter = :test + have_enqueued_job; never assert side-effects imperatively |
| External HTTP | WebMock / VCR; never allow real network in CI |
| DB state bleed | Transactional fixtures or DatabaseCleaner; never share let! across contexts |
| Race conditions | Explicit Capybara waits; avoid sleep |
| Imprecise assertions | change.from().to() over final state; exact values over be_truthy/be_falsey; never assert updated_at |
development
Entry point for Rails development workflows covering TDD, RSpec, Service Objects, DDD, GraphQL, Engines, and Code Quality. Use when the user asks about Ruby on Rails development patterns, needs RSpec test suites generated, wants service objects scaffolded, is setting up GraphQL schemas, performing Rails code review, refactoring .rb files, working with domain-driven design, implementing background jobs, conducting Rails security checks, or building Rails engines. Generates RSpec tests, structures service objects, enforces TDD workflows, configures GraphQL schemas, and coordinates domain-driven design patterns. Trigger keywords: Rails, RSpec, TDD, Rails testing, Rails refactor, Rails API, Rails code review, domain driven design, service objects, GraphQL, Rails engine, Ruby, .rb, background jobs, Rails migrations, Rails security check.
development
Use when shipping a Rails engine gem — FIRST run full test suite (`bundle exec rspec`) and fix ALL failures, verify gemspec metadata and dependencies match tested Rails/Ruby versions, dry-run: `gem build *.gemspec && gem push --dry-run *.gem` and verify contents, generate CHANGELOG.md organized by category (added/changed/deprecated/removed/fixed), produce step-by-step upgrade notes with before/after code, set semantic version in `lib/[engine_name]/version.rb`, document deprecations with migration paths, load release assets conditionally and state which one informed the output. Trigger words: version bump, changelog, deprecation, gemspec, upgrade, release, publish gem, ship gem.
development
Orchestrates the full Rails TDD cycle with hard gates: test MUST exist, be run, and FAIL for the correct reason (e.g. undefined method, not syntax error) before any implementation code — propose minimal implementation and wait for user approval → verify test PASSES → run full suite with rubocop, brakeman, rspec all green → produce YARD documentation and self-reviewed PR; phases context/test design→implementation→iterate→finish. Use when practicing test-driven development, red-green-refactor, TDD workflow, writing tests before code, adding tests first, or building a Rails feature where specs must gate implementation.
development
Complete Rails project setup loop with hard gates: verify Ruby version matches .ruby-version, Bundler installed, database connection successful, all env vars loaded, and ALL external CI actions pinned to immutable commit SHAs (never mutable tags like @v4) → configure CI/CD pipeline with linting, testing, and security scanning → validate end-to-end with bundle install, db:create, db:migrate, rspec, and write SETUP_CHECKLIST.md; phases context/onboarding→CI/CD configuration→environment validation. Use when starting a new Rails project, running `rails new`, configuring a Gemfile or .ruby-version, setting up a development environment, or wiring up CI/CD for a Ruby on Rails app. Trigger: setup project, new Rails app, configure CI/CD, dev environment setup, rails new, Gemfile setup, .ruby-version, Ruby on Rails project bootstrap.