| name | rails-tdd-standards |
| description | RSpec testing standards and best practices for Rails applications. Use when writing new tests, reviewing test quality, debugging factory errors, setting up FactoryBot, or enforcing single-expectation patterns. Also use when a test fails due to factory misconfiguration, wrong association keys, or missing role traits. Triggers on phrases like "write a test", "add specs", "factory error", "test is failing", "how should I test this", or when reviewing test code in a Rails project. |
| metadata | {"clawdbot":{"emoji":"🧪","requires":{"bins":["bundle"]},"os":["linux","darwin","win32"]}} |
Rails TDD Standards
Best practices for writing clean, reliable RSpec tests in Rails applications.
Core Principle: Single Expectation
One assertion per test. Tests should read like specifications — each it block verifies exactly one thing.
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to belong_to(:user) }
it "validates the user" do
expect(user).to validate_presence_of(:email)
expect(user).to validate_presence_of(:name)
expect(user).to be_valid
end
FactoryBot Patterns
Always use role traits
create(:user, :admin)
create(:user, :driver)
create(:user, :patron)
create(:user)
Association keys matter
Check your factory definitions carefully. Wrong keys cause silent failures.
create(:profile, owner: user)
create(:profile, user: user)
Always set required associations
When a model requires a specific association to be valid, always set it explicitly — don't rely on factory defaults when they might be nil or wrong.
let(:record) do
create(:model, required_association: other_record)
end
let(:record) { create(:model) }
Use described_class not hardcoded class names
subject { described_class.new(params) }
subject { MyService.new(params) }
Common FactoryBot Gotchas
Join tables without primary key
Tables with id: false can't use .last or .first.
record = JoinModel.find_by(field_a: a, field_b: b)
record = JoinModel.last
RecordInvalid from missing role/trait
If you see Validation failed: X must have Y role — you're missing a trait on the user factory.
user = create(:user, :driver)
user = create(:user)
Spec Structure
RSpec.describe MyClass do
subject(:instance) { described_class.new(params) }
let(:user) { create(:user, :admin) }
describe "#method_name" do
context "when condition is true" do
it "does the expected thing" do
expect(instance.method_name).to eq(expected)
end
end
context "when condition is false" do
it "does something else" do
expect(instance.method_name).to be_nil
end
end
end
end
Mocking & Stubbing
allow(object).to receive(:method_name).and_return(value)
expect(object).to receive(:method_name).once
stub_request(:post, "https://api.example.com/endpoint")
.to_return(status: 200, body: { result: "ok" }.to_json)
WebMock.disable_net_connect!(allow_localhost: true)
Service Object Testing
RSpec.describe MyService do
describe "#call" do
context "with valid params" do
it "returns the expected result" do
result = described_class.new(valid_params).call
expect(result).to be_a(ExpectedClass)
end
it "creates the expected record" do
expect { described_class.new(valid_params).call }
.to change(Record, :count).by(1)
end
end
context "with invalid params" do
it "returns false" do
expect(described_class.new(invalid_params).call).to be(false)
end
end
end
end
Running Tests
bundle exec rspec
bundle exec rspec spec/models/user_spec.rb
bundle exec rspec spec/models/user_spec.rb:42
bundle exec rspec --only-failures
bundle exec rspec --format documentation
See Also
references/factory-patterns.md — advanced FactoryBot patterns
references/system-specs.md — Capybara / browser testing setup