rspec-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

RSpec Best Practices

RSpec 最佳实践

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.
当你需要编写、评审或整理RSpec测试时,可以使用本技能。
核心原则: 优先保证行为可信度,而非实现耦合。优质的测试用例应具备可读性、确定性,且易于维护。

Quick Reference

快速参考

AspectRule
Spec typeRequest > controller; model for domain; system only for critical E2E
AssertionsTest behavior, not implementation
FactoriesMinimal — only attributes needed for the test
MockingStub external boundaries, not internal code
IsolationEach example independent; no shared mutable state
Naming
describe
for class/method,
context
for scenario
Service specsRequired:
describe '.call'
and
subject(:result)
for the primary invocation
let
vs
let!
Default to
let
. Use
let!
ONLY when the object must exist before the example runs (e.g., a DB record checked via
.count
)
External service mockingClass methods:
allow(ServiceClass).to receive(:method)
not
instance_double
. Use
instance_double
only for injected instance collaborators
Example namesNever use "and" in an example name — one behavior per example; split it
First sliceStart at the highest-value boundary that proves behavior
TDDWrite test first, run it, verify failure, then implement
方面规则
测试类型Request > controller;model用于领域逻辑;仅在关键端到端场景使用system测试
断言测试行为,而非实现细节
工厂仅保留测试所需的最少属性
模拟对外部边界进行Stub,而非内部代码
隔离性每个测试示例相互独立;无共享可变状态
命名
describe
用于类/方法,
context
用于场景
服务测试必填: 使用
describe '.call'
subject(:result)
定义主要调用逻辑
let
vs
let!
默认使用
let
。仅当对象必须在测试示例运行前存在时(例如通过
.count
检查数据库记录)才使用
let!
外部服务模拟类方法:使用
allow(ServiceClass).to receive(:method)
不要使用
instance_double
。仅对注入的实例协作对象使用
instance_double
测试示例名称测试示例名称中绝不要使用“and” — 每个示例对应一个行为;拆分多行为示例
首个测试切片从能以最少配置验证行为的最高价值边界开始
TDD先编写测试,运行测试,验证失败,再进行实现

HARD-GATE: Tests Gate Implementation

硬性要求:测试是实现的前置关卡

text
THE WORKFLOW IS: PRD → TASKS → TESTS → IMPLEMENTATION

Tests are a GATE between planning and code.
NO implementation code may be written until:
  1. The test EXISTS
  2. The test has been RUN
  3. The test FAILS for the correct reason (feature missing, not typo)
Write code before the test? Delete it. Start over.
The gate cycle for each behavior:
  1. Write test: One minimal test showing what the behavior should do
  2. Run test: Execute it — this is mandatory, not optional
  3. Validate failure: Confirm it fails because the feature is missing
  4. CHECKPOINT — Test Design Review: Present the failing test. Confirm boundary, behavior, and edge cases before writing implementation. See
    rails-tdd-slices
    for checkpoint format.
  5. GATE PASSED — you may now write implementation code
  6. CHECKPOINT — Implementation Proposal: Before writing code, state which classes/methods will be created or changed and the rough structure. Wait for confirmation.
  7. Write minimal code: Simplest implementation to make the test pass
  8. Run test again: Confirm it passes and no other tests break
  9. Refactor: Clean up — tests must stay green
  10. Next behavior: Return to step 1
text
工作流程为:PRD → 任务 → 测试 → 实现

测试是规划与代码之间的关卡。
在满足以下条件前,不得编写任何实现代码:
  1. 测试已存在
  2. 测试已运行
  3. 测试因正确原因失败(功能缺失,而非拼写错误)
先写代码再写测试?删除代码,重新开始。
每个行为的关卡循环:
  1. 编写测试: 编写一个最小化的测试,明确该行为应实现的功能
  2. 运行测试: 执行测试 — 这是强制要求,而非可选步骤
  3. 验证失败: 确认测试因功能缺失而失败
  4. 检查点 — 测试设计评审: 展示失败的测试。在编写实现前,确认边界、行为和边缘案例。查看
    rails-tdd-slices
    了解检查点格式。
  5. 通过关卡 — 你现在可以编写实现代码了
  6. 检查点 — 实现方案: 编写代码前,说明将创建或修改的类/方法以及大致结构。等待确认。
  7. 编写最小化代码: 用最简单的实现让测试通过
  8. 再次运行测试: 确认测试通过且未破坏其他测试
  9. 重构: 清理代码 — 测试必须保持通过状态
  10. 下一个行为: 返回步骤1

TDD Slice Selection

TDD 测试切片选择

Choose the first failing spec at the boundary that gives the strongest signal with the least setup:
Change typeBest first spec
New endpoint, controller action, or API behaviorRequest spec
New domain rule on an existing modelModel spec
New service object or orchestration flowService spec
Background job behaviorJob spec; add service/domain spec if logic is non-trivial
Rails engine route, install, or generator behaviorEngine request/routing/generator spec via
rails-engine-testing
Bug fixReproduction spec at the boundary where the bug is observed
选择能以最少配置提供最强验证信号的边界作为首个失败测试:
变更类型最优首个测试
新端点、控制器动作或API行为Request测试
现有模型的新领域规则Model测试
新服务对象或编排流程Service测试
后台任务行为Job测试;若逻辑复杂则补充service/领域测试
Rails引擎路由、安装或生成器行为通过
rails-engine-testing
编写引擎request/路由/生成器测试
Bug修复在观察到Bug的边界编写复现测试

Structure and Style

结构与风格

  • describe for the class, module, or behavior; context for scenarios ("when valid", "when user is missing").
  • Mirror source paths under
    spec/
    (e.g.
    app/models/user.rb
    spec/models/user_spec.rb
    ).
  • Use shared_examples / shared_context for repeated behavior; put reusable shared examples under
    spec/support/
    .
  • Use
    let_it_be
    only when
    test-prof
    already exists in the project.
  • Time-dependent behavior MUST use
    travel_to
    — do not set dates in the past as a shortcut, do not stub
    Time.now
    . Wrap assertions in a
    travel_to
    block to control the clock:
ruby
let(:subscription) { create(:subscription, activated_at: Time.current) }

context 'after expiration' do
  it 'is expired' do
    travel_to 31.days.from_now do
      expect(subscription).to be_expired
    end
  end
end
Minimal request spec skeleton:
ruby
undefined
  • describe 用于类、模块或行为;context 用于场景(如"when valid"、"when user is missing")。
  • spec/
    目录下镜像源码路径(例如
    app/models/user.rb
    spec/models/user_spec.rb
    )。
  • 对重复行为使用shared_examples / shared_context;将可复用的共享示例放在
    spec/support/
    目录下。
  • 仅当项目中已存在
    test-prof
    时,才使用
    let_it_be
  • 时间相关行为必须使用
    travel_to
    — 不要通过设置过去的日期走捷径,不要Stub
    Time.now
    。将断言包裹在
    travel_to
    块中以控制时间:
ruby
let(:subscription) { create(:subscription, activated_at: Time.current) }

context 'after expiration' do
  it 'is expired' do
    travel_to 31.days.from_now do
      expect(subscription).to be_expired
    end
  end
end
最小化Request测试骨架:
ruby
undefined

frozen_string_literal: true

frozen_string_literal: true

RSpec.describe 'POST /orders', type: :request do let(:product) { create(:product, stock: 5) }
context 'when product is in stock' do it 'returns 201 for an in-stock product' do post orders_path, params: { order: { product_id: product.id } }, as: :json expect(response).to have_http_status(:created) end end end

**Monolith vs engine:** When the project is a Rails engine, use `rails-engine-testing` for dummy-app setup and engine request/routing/generator specs; keep using this skill for general RSpec style.

For more examples (model spec, service spec, shared_examples, travel_to), see [EXAMPLES.md](./EXAMPLES.md).
RSpec.describe 'POST /orders', type: :request do let(:product) { create(:product, stock: 5) }
context 'when product is in stock' do it 'returns 201 for an in-stock product' do post orders_path, params: { order: { product_id: product.id } }, as: :json expect(response).to have_http_status(:created) end end end

**单体应用 vs 引擎:** 若项目是Rails引擎,使用`rails-engine-testing`进行dummy-app配置和引擎request/路由/生成器测试;通用RSpec风格仍遵循本技能规范。

更多示例(model测试、service测试、shared_examples、travel_to)请参见 [EXAMPLES.md](./EXAMPLES.md)。

Pitfalls

常见陷阱

PitfallWhat to do
Starting with the lowest layer by habitBegin at the boundary that proves the behavior users care about
Testing mock behavior instead of real behaviorAssert outcomes, not implementation details
Recommending
let_it_be
in every repo
Only use it when
test-prof
already exists in the project
Factories creating large graphs by defaultMinimal factories — only what the test needs
Setting dates in the past instead of
travel_to
Always use
travel_to
for time-dependent assertions — it makes boundary conditions deterministic
Code written before the testDelete it. Reproduction step isn't done yet.
Test name contains "and"One behavior per example. Split it.
陷阱解决方法
习惯性从最底层开始从用户关心的行为边界开始
测试模拟行为而非真实行为断言结果,而非实现细节
推荐在所有仓库使用
let_it_be
仅当项目中已存在
test-prof
时才使用
工厂默认生成大量关联对象保持工厂最小化 — 仅保留测试所需内容
设置过去的日期而非使用
travel_to
时间相关断言始终使用
travel_to
— 这能让边界条件具备确定性
先写代码再写测试删除代码。复现步骤尚未完成。
测试名称包含"and"每个示例对应一个行为。拆分多行为示例。

Integration

集成

SkillWhen to chain
rails-tdd-slicesWhen the hardest part is choosing the first failing Rails spec or vertical slice
rails-bug-triageWhen a bug report must be turned into a reproducible failing spec and fix plan
rspec-service-testingFor service object specs —
instance_double
for injected instance collaborators, hash factories, shared_examples; NOT for external class method mocking
rails-engine-testingFor engine specs — dummy app, routing specs, generator specs
rails-code-reviewWhen reviewing test quality as part of code review
refactor-safelyWhen adding characterization tests before refactoring
技能何时关联使用
rails-tdd-slices当难以选择首个失败的Rails测试或垂直切片时
rails-bug-triage当需要将Bug报告转化为可复现的失败测试和修复方案时
rspec-service-testing用于服务对象测试 — 对注入的实例协作对象使用
instance_double
、哈希工厂、shared_examples;不用于外部类方法模拟
rails-engine-testing用于引擎测试 — dummy app、路由测试、生成器测试
rails-code-review当在代码评审中检查测试质量时
refactor-safely当在重构前添加特征测试时