mcpField
R
rspec-best-practices

rspec-best-practices

github.com/midudevTesting & quality7.0k62 installsUpdated 1w ago
Repository

About rspec-best-practices

rspec-best-practices is a skill for writing, reviewing, and refining RSpec tests. It provides conventions for test structure, factories, mocking, determinism, and a TDD workflow.

Key features

  1. 01Guidance for spec types and structure
  2. 02Behavior-focused assertions
  3. 03Minimal factory and mocking practices
  4. 04Deterministic test recommendations
  5. 05Step-by-step TDD workflow
  6. 06Output rules and reusable templates

Use cases

  1. 01Writing RSpec tests for Rails behavior
  2. 02Reviewing specs for maintainability
  3. 03Reducing flaky and time-dependent tests

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/midudev/autoskills --skill rspec-best-practices

After installing, type/rspec-best-practicesin Claude Code to confirm it loaded.

Bundled filesassets/spec_templates.mdEXAMPLES.md

Details

From the project README

RSpec Best Practices

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.

Quick Reference

AspectRule
Spec typesModel: domain logic; Request: HTTP endpoints (prefer over controller); Job: background processing; Service/PORO: no Rails helpers; System: critical E2E only (slow)
AssertionsTest behavior, not implementation
FactoriesMinimal — only attributes needed; use traits for optional states; prefer build/build_stubbed over create
MockingStub external boundaries, not internal code
IsolationEach example independent; no shared mutable state
Namingdescribe for class/method, context for scenario
Service specsRequired: 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 mockingallow(ServiceClass).to receive(:method) — not instance_double; instance_double only for injected collaborators
Example namesPresent tense: it 'returns the user', never it 'should ...'; NEVER contains the word "and" — split into separate examples
aggregate_failuresUse when asserting multiple related items in one example

TDD Workflow

When driving new behaviour with RSpec, follow this sequence:

  1. Write the failing spec — pick the smallest spec type that exercises the intended behaviour (model > service > request > system).
  2. Run it and confirm the failure message — the error should be about missing code, not a setup problem.
  3. Implement the minimum code to make the spec pass.
  4. Refactor — clean up duplication and naming while keeping the suite green.
  5. Verify — run the full relevant spec file, then the suite, before committing.
Choosing the best first failing spec for a Rails change
Change typeStart with
Pure domain logicModel or PORO service spec
HTTP endpoint behaviourRequest spec
Background processingJob spec
Cross-layer user journeySystem spec (sparingly)

Factory Design

Minimal factories only. Never rely on factory defaults for business logic — set explicitly or use traits. Avoid create when build/build_stubbed suffices.

Service Spec (anchor pattern)

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

Shared Examples

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

One Behavior Per Example — NEVER "and" in Example Names

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

Output Style

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.

  1. Spec file path mirrors the source: app/foo/bar.rb → spec/foo/bar_spec.rb.
  2. # frozen_string_literal: true as the first line of every spec file.
  3. RSpec.describe uses the full constant path (RSpec.describe Module::Class do), not a string.
  4. describe '#method' / describe '.class_method' for each method under test.
  5. context 'when ...' / context 'with ...' for scenario variations — never use context to group methods.
  6. let for test data, let! ONLY when the object must exist before the action under test.
  7. No let_it_be unless the project already depends on test-prof (check Gemfile.lock first).
  8. NO "and" in any example description — split on every occurrence (see section above). This is the most-missed rule; do an explicit scan before returning the spec.
  9. subject(:result) { ... } for service / PORO specs invoking .call.
  10. travel_to / freeze_time for any time-dependent assertion — never set past Time.now or stub Time.current directly.
  11. External boundaries mocked at the class-method level (allow(SomeClient).to receive(:method)); ActiveRecord finders are NEVER mocked.

Flaky Tests & Deterministic Assertions

CauseFix
Time-dependent logicfreeze_time / travel_to; never set past dates as shortcut
State leakageEach example sets up own state; avoid before(:all)
Async jobsqueue_adapter = :test + have_enqueued_job; never assert side-effects imperatively
External HTTPWebMock / VCR; never allow real network in CI
DB state bleedTransactional fixtures or DatabaseCleaner; never share let! across contexts
Race conditionsExplicit Capybara waits; avoid sleep
Imprecise assertionschange.from().to() over final state; exact values over be_truthy/be_falsey; never assert updated_at

Assets

Comments

Markdown supported