关于 minitest-coder
minitest-coder 是用于在 Rails 项目中编写和组织 Minitest 测试的技能指南,面向 Rails 开发者。内容涵盖测试风格、fixtures、断言、覆盖范围,以及保持测试清晰易维护的模式。
主要功能
- 01传统与 spec 风格测试
- 02AAA 结构与行为导向测试
- 03Rails 测试文件组织
- 04fixtures、stub 与 mock
- 05断言和覆盖范围指导
- 06进阶模式与反模式
适用场景
- 01为 Rails 模型和服务编写测试
- 02为测试场景选择 Minitest 风格
- 03检查测试结构与覆盖范围
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill minitest-coder安装后在 Claude Code 中输入/minitest-coder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-patterns.mdanti-patterns.mdspec-patterns.mdtesting-tips.md详细介绍
Minitest 编码指南
核心理念
- AAA 模式:采用 Arrange-Act-Assert(准备、执行、断言)结构,清晰明了
- 关注行为,而非实现:测试代码的行为,而不是实现方式
- 隔离性:测试应彼此独立
- 描述性名称:使用清晰的测试描述
- 覆盖率:测试正常路径和边缘情况
- 快速测试:尽量减少数据库操作
- Fixtures:使用 fixtures 提供测试数据
Minitest 风格
| 风格 | 最适用于 | 语法 |
|---|---|---|
| 传统风格 | 简单的单元测试 | test "description" |
| Spec 风格 | 带上下文的复杂场景 | 使用 let/subject 的 describe/it |
传统风格
class UserTest < ActiveSupport::TestCase
test "validates presence of name" do
user = User.new
assert_not user.valid?
assert_includes user.errors[:name], "can't be blank"
end
end
Spec 风格
class UserTest < ActiveSupport::TestCase
describe "#full_name" do
subject { user.full_name }
let(:user) { User.new(first_name: "Buffy", last_name:) }
describe "with last name" do
let(:last_name) { "Summers" }
it "returns full name" do
assert_equal "Buffy Summers", subject
end
end
end
end
测试组织
文件结构
test/models/- 模型单元测试test/services/- 服务对象测试test/integration/- 全栈测试test/mailers/- Mailer 测试test/jobs/- 后台作业测试test/fixtures/- 测试数据test/test_helper.rb- 配置
命名约定
- 与应用结构对应:
app/models/user.rb→test/models/user_test.rb - 使用完整限定的命名空间:
class Users::ProfileServiceTest - 不要添加
require 'test_helper'(会自动导入)
Spec 风格模式
详细示例请参见 references/spec-patterns.md。
| 模式 | 使用场景 |
|---|---|
subject { ... } | 正在测试的方法 |
let(:name) { ... } | 延迟求值的数据 |
describe "context" | 将相关测试分组(最多 3 层) |
before { ... } | 复杂的初始化设置 |
describe "#process" do
subject { processor.process }
let(:processor) { OrderProcessor.new(order) }
let(:order) { orders(:paid_order) }
it "succeeds" do
assert subject.success?
end
end
Fixtures
# test/fixtures/users.yml
alice:
name: Alice Smith
email: [email protected]
created_at: <%= 2.days.ago %>
class UserTest < ActiveSupport::TestCase
fixtures :users
test "validates uniqueness" do
duplicate = User.new(email: users(:alice).email)
assert_not duplicate.valid?
end
end
模拟与桩
详细示例请参见 references/spec-patterns.md。
| 方法 | 用途 |
|---|---|
Object.stub :method, value | 模拟返回值 |
Minitest::Mock.new | 验证方法调用 |
test "processes payment" do
PaymentGateway.stub :charge, true do
processor = OrderProcessor.new(order)
assert processor.process
end
end
断言速查
基本断言
# Boolean
assert user.valid?
assert_not user.admin?
# Equality
assert_equal "Alice", user.name
assert_nil user.deleted_at
# Collections
assert_includes users, admin_user
assert_empty order.items
# Exceptions
assert_raises ActiveRecord::RecordInvalid do
user.save!
end
Rails 断言
# Changes
assert_changes -> { user.reload.status }, to: "active" do
user.activate!
end
assert_difference "User.count", 1 do
User.create(name: "Charlie")
end
# Responses
assert_response :success
assert_redirected_to user_path(user)
AAA 模式
test "processes refund" do
# Arrange
order = orders(:completed_order)
original_balance = order.user.account_balance
# Act
result = order.process_refund
# Assert
assert result.success?
assert_equal "refunded", order.reload.status
end
使用 subject 的 Spec 风格:
describe "#process_refund" do
subject { order.process_refund }
let(:order) { orders(:completed_order) }
it "updates status" do
subject
assert_equal "refunded", order.reload.status
end
it "credits user" do
assert_changes -> { order.user.reload.account_balance }, by: order.total do
subject
end
end
end
测试覆盖标准
| 类型 | 测试内容 |
|---|---|
| 模型 | 验证、关联、作用域、回调、方法 |
| 服务 | 正常路径、异常路径、边缘情况、外部集成 |
| 控制器 | 状态码、重定向、参数处理 |
| 作业 | 执行、重试逻辑、错误处理 |
覆盖率示例
class UserTest < ActiveSupport::TestCase
# Validations
test "validates presence of name" do
user = User.new(email: "[email protected]")
assert_not user.valid?
assert_includes user.errors[:name], "can't be blank"
end
# Methods
describe "#full_name" do
subject { user.full_name }
let(:user) { User.new(first_name: "Alice", last_name: "Smith") }
it "returns full name" do
assert_equal "Alice Smith", subject
end
describe "without last name" do
let(:user) { User.new(first_name: "Alice") }
it "returns first name only" do
assert_equal "Alice", subject
end
end
end
end
高级模式
有关 37signals 经过生产环境验证的模式的详细示例,请参见 references/advanced-patterns.md。
| 模式 | 解决的问题 |
|---|---|
| Current.account fixtures | 多租户 URL 隔离 |
| 验证断言的辅助方法 | 尽早失败并提供清晰的消息 |
| 确定性 UUID | 可预测的 fixture 排序 |
| VCR 时间戳过滤 | 可重复使用的 API 录制文件 |
| 基于线程的并发 | 检测竞态条件 |
| 适配器感知的辅助方法 | SQLite/MySQL 兼容性 |
反模式
详细示例请参见 references/anti-patterns.md。
| 反模式 | 不妥之处 |
|---|---|
require 'test_helper' | 会自动导入 |
| 嵌套层级 >3 | 输出难以阅读 |
用 @ivars 替代 let | 状态泄漏 |
缺少 subject | 代码重复 |
assert x.include?(y) | 应使用 assert_includes |
| 测试私有方法 | 与实现耦合 |
| 不使用 fixtures | 测试速度慢 |
最佳实践清单
组织结构:
- 文件与应用结构保持一致
- 不添加
require 'test_helper' - 使用完全限定的命名空间
风格选择:
- 简单测试使用传统风格
- 复杂上下文使用 Spec 风格
- 嵌套层级最多为 3 层
测试数据:
- 使用 fixtures(而非 factories)
- 使用
let共享数据 - 使用
subject指定被测方法
断言:
- 使用正确的断言方法
- 使用 Rails 辅助方法(
assert_changes、assert_difference) - 测试行为,而非实现
覆盖范围:
- 测试成功路径
- 测试失败路径
- 覆盖边界情况
何时选择不同风格
传统风格:简单验证、直接明了的测试、没有共享设置
Spec 风格:多个上下文、需要惰性求值、嵌套场景、可复用的 subject
如果能提升清晰度,可以在同一个文件中混用两种风格。
评论
支持 Markdown