mcpField
R
rspec-best-practices

rspec-best-practices

github.com/midudev测试与质量7.0k62 次安装1 周前更新
代码仓库

关于 rspec-best-practices

rspec-best-practices 是用于编写、审查和整理 RSpec 测试的技能,涵盖测试结构、工厂、模拟、确定性及 TDD 流程。

主要功能

  1. 01测试类型与结构规范
  2. 02以行为为中心的断言
  3. 03精简工厂与模拟实践
  4. 04确定性测试建议
  5. 05分步 TDD 流程
  6. 06输出规范与复用模板

适用场景

  1. 01为 Rails 功能编写 RSpec 测试
  2. 02审查并维护测试规范
  3. 03减少脆弱及时间相关测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/midudev/autoskills --skill rspec-best-practices

安装后在 Claude Code 中输入/rspec-best-practices确认已加载。

附带文件assets/spec_templates.mdEXAMPLES.md

详细介绍

RSpec 最佳实践

在编写、审查或清理 RSpec 测试时使用此技能。

核心原则: 优先确保行为可信,而不是耦合实现。好的规格应易于阅读、结果确定且维护成本低。

快速参考

方面规则
规格类型Model:领域逻辑;Request:HTTP 端点(优先于 controller);Job:后台处理;Service/PORO:不使用 Rails 辅助方法;System:仅用于关键端到端测试(速度较慢)
断言测试行为,而非实现
工厂尽量精简——只包含所需属性;可选状态使用 traits;优先使用 build/build_stubbed 而非 create
模拟对外部边界进行 stub,而非内部代码
隔离每个示例彼此独立;不共享可变状态
命名用 describe 描述类/方法,用 context 描述场景
服务规格必需: 使用 describe '.call' 和 subject(:result) 表示主要调用
let 与 let!默认使用 let;仅当对象必须在示例运行前存在时才使用 let!
外部服务模拟使用 allow(ServiceClass).to receive(:method)——不要使用 instance_double;instance_double 仅用于注入的协作者
示例名称使用现在时:it 'returns the user',绝不要写 it 'should ...';绝不包含单词 "and"——拆分为单独的示例
aggregate_failures在一个示例中断言多个相关项目时使用

TDD 工作流

使用 RSpec 推进新行为时,按以下顺序进行:

  1. 编写失败的规格——选择能够验证预期行为的最小规格类型(model > service > request > system)。
  2. 运行并确认失败消息——错误应与缺少代码有关,而不是设置问题。
  3. 实现使规格通过所需的最少代码。
  4. 重构——在保持测试套件通过的同时,清理重复代码并改进命名。
  5. 验证——提交前先运行相关的完整规格文件,再运行整个测试套件。
为 Rails 更改选择最佳的首个失败规格
更改类型从以下规格开始
纯领域逻辑Model 或 PORO 服务规格
HTTP 端点行为Request 规格
后台处理Job 规格
跨层用户旅程System 规格(谨慎使用)

工厂设计

工厂应尽量精简。绝不要依赖工厂默认值实现业务逻辑——应显式设置或使用 traits。当 build/build_stubbed 足以满足需求时,避免使用 create。

服务规格(锚点模式)

RSpec.describe Invoices::MarkOverdue do
  describe '.call' do
    subject(:result) { described_class.call(invoice: invoice) }

    context 'when the invoice is overdue and unpaid' do
      let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: nil) }

      it 'marks the invoice overdue' do
        expect { result }.to change { invoice.reload.overdue? }.from(false).to(true)
      end
    end

    context 'when the invoice is already paid' do
      let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: 1.day.ago) }

      it 'does not change the invoice' do
        expect { result }.not_to change { invoice.reload.updated_at }
      end
    end
  end
end

→ 完整示例:EXAMPLES.md | 可复制粘贴的模板:assets/spec_templates.md

共享示例

仅当同一行为契约适用于多个 subject,且无需为每个示例覆盖 let 时才使用。若每个 context 都需要不同的设置,则应避免使用——这说明抽象方式不合适。→ 示例见 EXAMPLES.md

每个示例只测一种行为——示例名称中绝不出现 "and"

it / specify 描述中的单词 "and" 表示该示例断言了两种行为。应将其拆分。每个示例只测一种行为。适用于所有规格类型——model、request、service、job、mailer、system。

# BAD — two assertions; if the first fails, the second never runs
it 'returns 201 and creates the record' do; end
it 'saves the order and sends the confirmation email' do; end
it 'updates the user and logs the change' do; end

# GOOD — one observable outcome per example
it 'returns 201' do; end
it 'creates the record' do; end

it 'saves the order' do; end
it 'sends the confirmation email' do; end

在完成任何规格前进行自查: 检查每个 it '...' / it "..." / specify '...' 字符串,查找单词 and(不区分大小写,按单词边界匹配)。每一处都必须拆分——即使是 'returns nil and does not raise' 这类“为了方便”而写的示例,也没有例外。

输出风格

当要求编写或审查 RSpec 规范时,输出必须满足以下每条规则。每条规则单独评分——违反一条就会导致整个检查不通过。

  1. 规范文件路径与源文件路径对应:app/foo/bar.rb → spec/foo/bar_spec.rb。
  2. 每个规范文件的第一行必须是 # frozen_string_literal: true。
  3. RSpec.describe 使用完整常量路径(RSpec.describe Module::Class do),而非字符串。
  4. 对每个被测试的方法使用 describe '#method' / describe '.class_method'。
  5. 对场景变化使用 context 'when ...' / context 'with ...'——绝不使用 context 来归类方法。
  6. 测试数据使用 let;只有在被测操作执行前必须创建对象时,才使用 let!。
  7. 除非项目已经依赖 test-prof,否则不要使用 let_it_be(请先检查 Gemfile.lock)。
  8. 任何示例描述中都不能出现 "and"——每次出现都要拆分(见上文)。这是最容易遗漏的规则;返回规范前请明确检查一遍。
  9. 对调用 .call 的服务 / PORO 规范,使用 subject(:result) { ... }。
  10. 对任何依赖时间的断言,使用 travel_to / freeze_time——绝不要将 Time.now 设为过去的时间,也不要直接对 Time.current 进行桩替换。
  11. 在类方法层级模拟外部边界(allow(SomeClient).to receive(:method));绝不模拟 ActiveRecord 查找器。

易波动测试与确定性断言

原因修复方法
依赖时间的逻辑使用 freeze_time / travel_to;绝不要用过去的日期图省事
状态泄漏每个示例自行设置状态;避免使用 before(:all)
异步任务queue_adapter = :test + have_enqueued_job;绝不以命令式方式断言副作用
外部 HTTP使用 WebMock / VCR;绝不允许 CI 中发起真实网络请求
数据库状态相互污染使用事务性 fixture 或 DatabaseCleaner;绝不要在不同上下文间共享 let!
竞态条件使用明确的 Capybara 等待;避免使用 sleep
断言不精确优先使用 change.from().to(),而非最终状态;优先使用精确值,而非 be_truthy/be_falsey;绝不断言 updated_at

资源

评论

支持 Markdown