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.
CODEBLOCK0
FactoryBot Patterns
Always use role traits
CODEBLOCK1
Association keys matter
Check your factory definitions carefully. Wrong keys cause silent failures.
CODEBLOCK2
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.
CODEBLOCK3
Use described_class not hardcoded class names
CODEBLOCK4
Common FactoryBot Gotchas
Join tables without primary key
Tables with
id: false can't use
.last or
.first.
CODEBLOCK5
RecordInvalid from missing role/trait
If you see
Validation failed: X must have Y role — you're missing a trait on the user factory.
CODEBLOCK6
Spec Structure
CODEBLOCK7
Mocking & Stubbing
CODEBLOCK8
Service Object Testing
CODEBLOCK9
Running Tests
CODEBLOCK10
See Also
- -
references/factory-patterns.md — advanced FactoryBot patterns - INLINECODE7 — Capybara / browser testing setup
Rails TDD 标准
在 Rails 应用中编写干净、可靠的 RSpec 测试的最佳实践。
核心原则:单一断言
每个测试一个断言。测试应像规范一样可读——每个 it 块只验证一件事。
ruby
✅ 正确
it { is
expected.to validatepresence_of(:email) }
it { is
expected.to belongto(:user) }
❌ 错误——一个测试中包含过多期望
it 验证用户 do
expect(user).to validate
presenceof(:email)
expect(user).to validate
presenceof(:name)
expect(user).to be_valid
end
FactoryBot 模式
始终使用角色特征
ruby
✅ 正确
create(:user, :admin)
create(:user, :driver)
create(:user, :patron)
❌ 错误——缺少角色上下文
create(:user)
关联键很重要
仔细检查你的工厂定义。错误的键会导致静默失败。
ruby
✅ 示例——如果你的工厂使用 owner:
create(:profile, owner: user)
❌ 错误的键
create(:profile, user: user) # 如果工厂期望 owner: 则会失败
始终设置必需的关联
当模型需要特定关联才能有效时,始终显式设置它——不要依赖可能为 nil 或错误的工厂默认值。
ruby
✅ 显式——意图清晰,无意外
let(:record) do
create(:model, required
association: otherrecord)
end
❌ 隐式——如果工厂默认值更改可能会出错
let(:record) { create(:model) }
使用 described_class 而非硬编码的类名
ruby
✅
subject { described_class.new(params) }
❌
subject { MyService.new(params) }
常见 FactoryBot 陷阱
无主键的连接表
使用 id: false 的表不能使用 .last 或 .first。
ruby
✅ 使用作用域查询
record = JoinModel.find
by(fielda: a, field_b: b)
❌ 会引发 ActiveRecord::MissingRequiredOrderError
record = JoinModel.last
因缺少角色/特征导致的 RecordInvalid
如果看到 Validation failed: X must have Y role——说明用户工厂缺少特征。
ruby
✅
user = create(:user, :driver)
❌ 导致必须是司机验证错误
user = create(:user)
测试结构
ruby
RSpec.describe MyClass do
# 主题
subject(:instance) { described_class.new(params) }
# 共享设置
let(:user) { create(:user, :admin) }
# 按行为分组
describe #method_name do
context 当条件为真时 do
it 执行预期操作 do
expect(instance.method_name).to eq(expected)
end
end
context 当条件为假时 do
it 执行其他操作 do
expect(instance.methodname).to benil
end
end
end
end
模拟与桩
ruby
桩方法
allow(object).to receive(:method
name).andreturn(value)
桩并验证其被调用
expect(object).to receive(:method_name).once
桩 HTTP 调用(WebMock)
stub_request(:post, https://api.example.com/endpoint)
.to
return(status: 200, body: { result: ok }.tojson)
允许系统测试使用 localhost(如果使用 WebMock)
WebMock.disable
netconnect!(allow_localhost: true)
服务对象测试
ruby
RSpec.describe MyService do
describe #call do
context 使用有效参数 do
it 返回预期结果 do
result = describedclass.new(validparams).call
expect(result).to be_a(ExpectedClass)
end
it 创建预期记录 do
expect { describedclass.new(validparams).call }
.to change(Record, :count).by(1)
end
end
context 使用无效参数 do
it 返回 false do
expect(describedclass.new(invalidparams).call).to be(false)
end
end
end
end
运行测试
bash
运行完整测试套件
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
参见
- - references/factory-patterns.md — 高级 FactoryBot 模式
- references/system-specs.md — Capybara / 浏览器测试设置