RSpec Testing skill for Codex

Specs that read as sentences and skip needless rows. Codex writes RSpec that reads as documentation and runs fast. It chooses build_stubbed over create unless the code queries the database, uses verifying doubles so stubs track the real API, replaces controller specs with request specs, and debugs order-dependent failures with the printed seed and --bisect instead of guessing.

  • Testing & QA
  • $rspec-testing
  • 241 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- rspec-testing

Installs to ~/.agents/skills/rspec-testing/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $rspec-testing, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and restructures RSpec suites for Ruby and Rails - describe/context/it layout, let vs let! vs before, verifying doubles, FactoryBot build_stubbed and traits, request specs over controller specs, system specs, ActiveJob and time helpers, and seed-based order debugging. Use when a Ruby or Rails app tests with RSpec, specs are slow or order-dependent, factories create too many records, or controller specs need replacing with request specs.

Use it when

  • A Ruby or Rails app uses RSpec
  • Controller specs need converting to request specs
  • Factories create too many records and slow the suite
  • Specs fail only in some random orders

Use something else when

How to check the result

The suite passes under two different random seeds, --profile 10 shows no factory-heavy slow examples, and doc-format output reads as clear sentences.

The mistake it prevents

Using create where build_stubbed would do, so every example inserts a chain of associated records and the suite slows to a crawl.

~/.agents/skills/rspec-testing/SKILL.md

--- name: rspec-testing description: Writes and restructures RSpec suites for Ruby and Rails - describe/context/it layout, let vs let! vs before, verifying doubles, FactoryBot… license: MIT ---

A healthy RSpec suite reads like a specification in --format documentation, runs in random order, creates database rows only when a test needs persistence, and tests HTTP behavior through request specs.

Inspect first

  1. Read AGENTS.md, then .rspec, spec/spec_helper.rb and spec/rails_helper.rb. Note config.order, use_transactional_fixtures, filter_run_when_matching, example_status_persistence_file_path, and support files loaded from spec/support.
  2. Run the command the project uses (bin/rspec, bundle exec rspec). Check for parallel_tests or knapsack in the Gemfile before adding parallelism.
  3. Look at spec/factories for existing factories and traits before adding new ones.
  4. Check whether config.disable_monkey_patching! is on; if so, use RSpec.describe.
# spec/spec_helper.rb
RSpec.configure do |config|
  config.disable_monkey_patching!
  config.order = :random
  Kernel.srand config.seed
  config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
  config.filter_run_when_matching :focus
  config.mock_with(:rspec) { |mocks| mocks.verify_partial_doubles = true }
  config.expect_with(:rspec) { |e| e.include_chain_clauses_in_custom_matcher_descriptions = true }
end

verify_partial_doubles makes allow(obj).to receive(:nonexistent) fail, which catches stubs that drifted from the real API.

Structure

RSpec.describe Checkout::Quote do
  subject(:quote) { described_class.new(cart, country:) }

  let(:cart) { build_stubbed(:cart, subtotal_cents: subtotal) }
  let(:country) { "DE" }

  context "when the subtotal reaches the free-shipping threshold" do
    let(:subtotal) { 10_000 }

    it "charges no shipping" do
      expect(quote.shipping_cents).to eq(0)
    end
  end

  context "when shipping abroad" do
    let(:subtotal) { 5_000 }
    let(:country) { "US" }

    it "adds the international rate" do
      expect(quote.shipping_cents).to eq(2_500)
    end
  end
end
  • describe names the thing; context starts with “when”, “with” or “without”; it states the outcome. The doc output then reads as sentences.
  • Override only the let that varies per context. Keep the value that explains the behavior visible in that context.
  • One behavior per example. Use aggregate_failures when several expectations describe one outcome (status plus body):
it "creates the order", :aggregate_failures do
  post "/orders", params: { sku: "A1" }, as: :json
  expect(response).to have_http_status(:created)
  expect(response.parsed_body).to include("status" => "pending")
end

let, let!, before

UseWhen
letlazily built value referenced by examples (default)
let!a record that must exist even though no example references it (a competing row for a uniqueness test)
beforean action, not a value: sign in, freeze time, stub a service
subject(:name)the object under test, named

Deep nesting with let overrides five levels up is a smell; flatten into clearer contexts.

Factories

FactoryBot.define do
  factory :order do
    customer
    status { "pending" }
    sequence(:number) { |n| "ORD-#{n.to_s.rjust(6, '0')}" }

    trait :paid do
      status { "paid" }
      paid_at { Time.current }
    end

    trait :with_items do
      transient { item_count { 2 } }
      after(:create) { |order, ctx| create_list(:line_item, ctx.item_count, order:) }
    end
  end
end
StrategyHits DBUse for
build_stubbedno, fakes an id and persisted?unit specs of models, presenters, services that only read
buildno (but associations may be created)validations, unsaved objects
createyesqueries, scopes, request and system specs
  • Default to build_stubbed. Switch to create only when the code queries the database.
  • Keep base factories minimal (only required attributes); add traits for optional state.
  • association in a factory creates the parent under create; a deep chain can insert dozens of rows per example. Profile with --profile 10 and watch for it.
  • For broader factory design across languages, hand off to $test-data-factories.

Doubles

let(:gateway) { instance_double(Payments::Gateway, charge: Result.success) }

it "charges the card once" do
  described_class.new(gateway:).call(order)
  expect(gateway).to have_received(:charge).with(order.total_cents).once
end
  • Use verifying doubles (instance_double, class_double) so stubbed methods must exist with a compatible arity.
  • Prefer allow + have_received (spy style) so arrange and assert stay in order.
  • Do not stub the object under test. Do not mock ActiveRecord query chains; use the database instead.

Request specs

Controller specs are discouraged for Rails 5+; write request specs.

RSpec.describe "Orders API", type: :request do
  let(:user) { create(:user) }
  let(:headers) { { "Authorization" => "Bearer #{token_for(user)}" } }

  describe "GET /orders/:id" do
    it "returns 404 for another customer's order" do
      order = create(:order)
      get "/orders/#{order.id}", headers:, as: :json
      expect(response).to have_http_status(:not_found)
    end
  end
end

Cover per endpoint: happy path, auth missing (401), forbidden (403/404), validation error (422) with error body shape, and pagination edges.

System specs

RSpec.describe "Checkout", type: :system do
  before { driven_by(:rack_test) }                     # fast default, no JS

  context "with the JavaScript payment form" do
    before { driven_by(:selenium, using: :headless_chrome) }   # only where JS is required

    it "completes with a saved card" do
      visit cart_path
      click_on "Pay now"
      expect(page).to have_content("Thank you")
    end
  end
end

Capybara matchers like have_content wait; expect(page.text).to include(...) does not. Never use sleep.

Time, jobs, mail

include ActiveSupport::Testing::TimeHelpers            # in rails_helper

it "expires invites after 7 days" do
  invite = create(:invite)
  travel_to(8.days.from_now) { expect(invite.reload).to be_expired }
end

it "enqueues the receipt" do
  expect { checkout.call }.to have_enqueued_job(ReceiptJob).with(order.id)
end

it "counts a new order" do
  expect { checkout.call }.to change(Order, :count).by(1)
end

Set ActiveJob::Base.queue_adapter = :test in the environment or spec helper for job matchers.

Running and debugging

bundle exec rspec spec/requests/orders_spec.rb:42      # one example by line
bundle exec rspec --only-failures                       # needs persistence file
bundle exec rspec --next-failure
bundle exec rspec --seed 12345                          # reproduce an order failure
bundle exec rspec --bisect --seed 12345                 # find the minimal polluting pair
bundle exec rspec --profile 10                          # slowest examples

Order-dependent failure: rerun with the printed seed, then --bisect. The polluter is usually a class-level cache, a stubbed constant, ENV mutation, or data committed outside the transaction (threads, after_commit with a separate connection).

Gotchas

  • let is memoized per example, not across examples; before(:all) / before_all shares state and must not create records unless wrapped in a transaction helper.
  • change, have_enqueued_job and raise_error need a block: expect { act }. Passing a value (expect(act)) runs the action too early and the matcher errors or checks nothing.
  • create inside let in a top-level group runs for every example that touches it, including ones that did not need it.
  • allow_any_instance_of hides design problems; inject the collaborator instead.

Verification

  1. Run the touched spec files, then the full suite with --order random.
  2. Run touched files twice with different seeds.
  3. --profile 10 shows no new slow examples dominated by factory creation.
  4. Break the behavior once and confirm the failure message names the expectation clearly.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "RSpec Testing"
  short_description: "Specs that read as sentences and skip needless rows"
  default_prompt: "$rspec-testing Write request specs for the OrdersController API"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem