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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rspec-testing Installs to ~/.agents/skills/rspec-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rspec-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rspec-testing Installs to .agents/skills/rspec-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rspec-testing.zipunzip -o rspec-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rspec-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The app uses Minitest and you want to keep it: use rails-apps
- You need general factory design across stacks: use test-data-factories
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.
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
- Read
AGENTS.md, then.rspec,spec/spec_helper.rbandspec/rails_helper.rb. Noteconfig.order,use_transactional_fixtures,filter_run_when_matching,example_status_persistence_file_path, and support files loaded fromspec/support. - Run the command the project uses (
bin/rspec,bundle exec rspec). Check forparallel_testsorknapsackin the Gemfile before adding parallelism. - Look at
spec/factoriesfor existing factories and traits before adding new ones. - Check whether
config.disable_monkey_patching!is on; if so, useRSpec.describe.
Recommended configuration
# 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
describenames the thing;contextstarts with “when”, “with” or “without”;itstates the outcome. The doc output then reads as sentences.- Override only the
letthat varies per context. Keep the value that explains the behavior visible in that context. - One behavior per example. Use
aggregate_failureswhen 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
| Use | When |
|---|---|
let | lazily 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) |
before | an 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
| Strategy | Hits DB | Use for |
|---|---|---|
build_stubbed | no, fakes an id and persisted? | unit specs of models, presenters, services that only read |
build | no (but associations may be created) | validations, unsaved objects |
create | yes | queries, scopes, request and system specs |
- Default to
build_stubbed. Switch tocreateonly when the code queries the database. - Keep base factories minimal (only required attributes); add traits for optional state.
associationin a factory creates the parent undercreate; a deep chain can insert dozens of rows per example. Profile with--profile 10and 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
letis memoized per example, not across examples;before(:all)/before_allshares state and must not create records unless wrapped in a transaction helper.change,have_enqueued_jobandraise_errorneed a block:expect { act }. Passing a value (expect(act)) runs the action too early and the matcher errors or checks nothing.createinsideletin a top-level group runs for every example that touches it, including ones that did not need it.allow_any_instance_ofhides design problems; inject the collaborator instead.
Verification
- Run the touched spec files, then the full suite with
--order random. - Run touched files twice with different seeds.
--profile 10shows no new slow examples dominated by factory creation.- 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.yaml
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.