关于 rust-testing-code-review
用于审查 Rust 测试代码的技能,涵盖测试组织、异步设置、断言、隔离与覆盖情况,并要求结合 Rust edition 检查及验证后再报告问题。
主要功能
- 01结合 Rust edition 的审查检查
- 02单元与集成测试组织
- 03异步测试和测试替身指导
- 04断言、隔离与错误路径检查
- 05并发及高级测试覆盖检查
适用场景
- 01审查 Rust crate 的测试布局
- 02检查异步测试和测试替身
- 03评估边界情况与错误路径覆盖
- 04审查并发测试实践
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ existential-birds/ beagle --skill rust-testing-code-review安装后在 Claude Code 中输入/rust-testing-code-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-testing.mdconcurrency-testing.mdintegration-tests.mdunit-tests.md详细介绍
Rust 测试代码审查
审查工作流
- 检查 Rust 版本 — 查看
Cargo.toml中的版本(2021 还是 2024)。版本 2024 会改变if let和尾表达式中的临时值作用域,并将#[expect]设为首选的 lint 抑制方式 - 检查测试组织方式 — 单元测试放在
#[cfg(test)]模块中,集成测试放在tests/目录中 - 检查异步测试设置 — 异步测试使用
#[tokio::test],并正确配置运行时。检查 mock 是否使用了async-trait,而原生 traitasync fn是否也可行 - 检查断言 — 确保消息有意义、断言类型正确。检查
if let断言是否受版本 2024 临时值作用域变化影响 - 检查测试隔离性 — 测试之间不得共享可变状态,并确保正确设置和清理。共享 fixture 优先使用
LazyLock,而不是lazy_static!/once_cell - 检查覆盖模式 — 确保测试覆盖错误路径和边界情况
门槛(硬性)
只有在满足每项通过条件(是/否,并提供具体凭据)后,才能进入 输出格式。
- 已记录版本 — 打开目标 crate 的
Cargo.toml(或工作区[workspace.package]/ 继承的版本),并记录edition值。在引用 Rust 2024 特有行为(if let/ 尾部临时值的析构、#[expect]与#[allow]的迁移、trait 中原生async fn作为默认方案)之前,通过条件:能够引用edition = "…"(或注明“继承自工作区”)。如果版本不是2024,不要将这些问题报告为版本 2024 的回归;如果仍有参考价值,最多列为信息性。 dyn与静态异步 mock — 建议使用 trait 中的原生async fn取代async-trait之前,检查 mock 是否用作dyn Trait。通过条件:如果必须使用dyn,则跳过该建议,或遵循有效模式(仍需使用async-trait)。- 验证协议 — 通过条件:列出任何问题之前,已完成 review-verification-protocol 技能中的步骤(参见提交前的发现)。
输出格式
按以下格式报告发现:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
快速参考
| 问题类型 | 参考 |
|---|---|
单元测试、断言、命名、快照、rstest、文档测试、#[expect]、LazyLock fixture、尾表达式作用域 | references/unit-tests.md |
集成测试、异步测试、fixture、测试数据库、原生 async fn mock、if let 临时值作用域 | references/integration-tests.md |
模糊测试、proptest、Miri、Loom 基础、mock 策略、stub/fake/mock/spy 分类、rstest 矩阵、paste!、build.rs 测试生成、criterion 基线 + black_box 使用规范、trybuild UI 测试、clippy lint 组 | references/advanced-testing.md |
| Loom 交错测试、Miri 未定义行为检查、shuttle、ThreadSanitizer、并发代码的 CI 矩阵 | references/concurrency-testing.md |
审查清单
测试结构
- 源文件中的
#[cfg(test)] mod tests内包含单元测试 - 集成测试放在
tests/目录中(每个模块或功能一个文件) - 测试模块中使用
use super::*访问父模块项 - 测试函数名称能描述场景:
test_<function>_<scenario>_<expected> - 测试彼此独立,不依赖执行顺序
异步测试
- 异步测试函数使用
#[tokio::test] - 测试多线程行为时使用
#[tokio::test(flavor = "multi_thread")] - 异步测试中不使用
block_on(直接使用.await) - 为可能挂起的测试设置超时
- mock trait 使用原生
async fn,而不是async-traitcrate(自 Rust 1.75 起稳定)
断言
- 使用
assert_eq!/assert_ne!进行值比较(错误信息比assert!更好) - 对不言自明的断言添加自定义消息
- 使用
matches!宏检查枚举变体 - 使用
matches!或模式匹配检查错误类型,而非比较字符串 - 条件允许时,每个测试只写一个断言(更容易诊断失败)
- 检查
if let断言是否受 edition 2024 临时值作用域影响——条件中的临时值会更早释放,可能导致借用失效 - 检查尾表达式返回值是否受 edition 2024 影响——尾表达式中的临时值会在局部变量之前释放
模拟与测试替身
- 使用 trait 作为依赖注入的隔离接口(而非具体类型)
- 将模拟实现保持精简——只实现测试所需的内容
- 不模拟不归你所有的类型(将外部依赖封装在自有 trait 后面)
- 使用辅助函数提供测试夹具,而非全局状态
- 使用
std::sync::LazyLock共享测试夹具,而非lazy_static!或once_cell(自 Rust 1.80 起稳定)
错误路径测试
- 测试
Result::Err变体,而不只是正常路径 - 检查具体的错误变体(而不只是检查“是否出错”)
- 谨慎使用
#[should_panic]——优先使用返回Result的测试
测试中的 Lint 抑制
- 对测试专用的抑制使用
#[expect(lint)],而非#[allow(lint)](自 Rust 1.81 起稳定) - 在测试代码中的每个
#[expect]或#[allow]上添加说明注释 - 将过时的
#[allow]属性迁移为#[expect],以便自动清理
测试命名
- 测试名称应像句子一样描述行为(而非
test_happy_path) - 使用嵌套的
mod块组织相关测试 - 测试名称遵循以下模式:
<function>_should_<behavior>_when_<condition>
快照测试
- 对复杂的结构化输出(JSON、YAML、HTML、CLI 输出)使用
cargo insta - 快照应小而聚焦(避免包含庞大对象)
- 对不稳定字段(时间戳、UUID)使用脱敏
- 将快照提交到 git 的
snapshots/目录 - 简单值使用
assert_eq!,而非快照
参数化测试
- 使用
rstest避免为相似输入编写重复的测试函数 - 使用
#[rstest]和#[case::name]属性编写带有描述性参数的测试 - 当多个测试需要相同的构造过程时,使用
#[fixture]共享测试设置 - 参数化测试仍应使用描述性的用例名称(而非仅使用
#[case(1)]) - 与异步结合时,使用
#[rstest] #[tokio::test]编写异步参数化测试
文档测试
- 公共 API 函数应包含
/// # Examples和可运行的代码 - 文档测试兼具文档说明和正确性检查的作用
- 使用以
#开头的隐藏设置代码行,让示例保持简洁 -
cargo test --doc应通过(nextest 不会运行文档测试)
并发测试
- 手写原子操作 /
unsafe impl Send|Sync/ 状态机应包含使用loom::sync和loom::thread替代实现(而非std::sync)的#[cfg(loom)]测试 - 使用
unsafe操作原子变量、指针或UnsafeCell的 crate,应在 CI 中针对每个 PR 运行cargo +nightly miri test --all-features(不能只在发布时运行,也不能使用模块级cfg_attr(miri, ignore)) - 无锁数据结构(
AtomicPtr栈/队列/链表)应包含 loom + Miri(Stacked Borrows 和 Tree Borrows)+ 由proptest驱动的操作序列测试 - 不确定性输入(
Instant::now、rand、环境变量、HashMap迭代)不得出现在loom::model函数体中 - Loom 任务应在
--release模式下运行,并设置LOOM_MAX_PREEMPTIONS上限;loom 测试应放在单独的tests/文件中,以免--cfg loom影响常规cargo test - 不得将
nextest run -j1作为覆盖竞态条件测试的证据;包含大量 FFI 的unsafe extern "C"路径应配置 ThreadSanitizer 任务
测试增强(Fake、Mock、Stub、Spy)
- 测试替身按用途准确分类:stub(预置数据)、fake(可运行但经过简化的实现)、mock(预设期望并进行验证)、spy(记录调用,供事后检查)——术语使用准确
- 在使用 fake 更简单的场景中,不使用
mockall风格的 mock(对于仓储类 trait,内存数据库优于充满期望配置的 mock) - Fake 使用与生产实现相同的契约进行测试(trait 一致性测试);不允许出现偏差
- 使用 Trait-as-seam 模式注入测试替身——生产代码依赖 trait,而非具体类型
性能测试(Criterion)
- 基准测试使用
criterion::black_box(...)防止常量折叠;指针类型的输入使用black_box(input.as_ptr()),而非black_box(&input) - 基准测试隔离 I/O——在
iter_batched的 setup 闭包中执行准备工作(打开文件、分配内存),而非在测量闭包中执行 - CI 使用
cargo bench -- --save-baseline main保存基线;功能分支使用--baseline main进行比较;定义并执行回归阈值 - Criterion 基准测试使用
--profile bench构建(启用发布优化,并保留用于火焰图关联的调试符号) - 不使用
#[bench](不稳定、已弃用)——仅使用criterion - 不引用单次计时结果作为证据;报告应展示 Criterion 统计引擎计算出的均值和方差
测试生成
- 表驱动输入使用
rstest和#[case::name(...)](测试输出中显示描述性案例名称) - 多维输入矩阵使用
rstest,在多个参数上使用#[values(...)](笛卡尔积),而非手动展开测试函数 - 大型语料库使用
paste!宏或build.rs代码生成,为每个文件/案例生成一个带有输入名称的测试 - 生成的测试具有稳定的名称,可在 CI 日志差异中保持不变
Proc-Macro UI 测试(trybuild)
- 会产生编译错误的 Proc-macro 使用
trybuild::TestCases覆盖每条失败路径,并提供.stderr参考文件 - 最新一次 rustc 升级后重新生成了
.stderr输出(否则 CI 可能出现虚假失败);在 CONTRIBUTING 中记录TRYBUILD=overwrite cargo test - CI 为 trybuild 任务固定特定的稳定版 rustc(
.stderr格式会随稳定版发布而变化) - 在 nightly 上跳过 trybuild 测试(nightly 诊断信息与稳定版不同)
Clippy Lint 组策略
-
clippy::correctness设为deny(始终如此——这些都是 bug) - 对库,将
clippy::suspicious设为warn或deny - 将
clippy::perf设为warn(可在热点路径上带来实际收益) - 对库启用
clippy::pedantic;抑制 lint 时使用带有说明的#[expect(clippy::lint_name, reason = "...")],而非直接使用#[allow] - CI 中不启用
clippy::nursery(实验性 lint;会随 rustc 版本变化) - 单独选择性启用
clippy::restrictionlint;绝不整体启用该组
严重性校准
严重
- 测试通过了,却没有实际验证行为(断言检查了错误的值)
- 测试间共享可变状态,导致结果不稳定
- 安全关键代码缺少错误路径测试
主要问题
- 未提供
expected消息的#[should_panic](会捕获任何 panic,包括不符合预期的 panic) - 在测试设置中使用
unwrap(),掩盖了实际失败位置 - 依赖执行顺序的测试
- 在断言中使用带内联临时值的
if let,会在 edition 2024 的临时值作用域规则下出问题 - 项目面向 edition 2024,且可使用 trait 中的原生
async fn时,仍在 mock trait 上使用async-trait
次要问题
- 复杂比较缺少断言消息
- 使用
assert!(x == y),而不是assert_eq!(x, y)(错误消息较差) - 测试名称未能说明测试场景
- 可提取为辅助函数的冗余设置代码
- 使用
#[allow],而非能自动清理抑制的#[expect] - 有
LazyLock可用时,仍使用lazy_static!或once_cell作为测试 fixture
信息性建议
- 建议通过
proptest或quickcheck添加基于属性的测试 - 建议为复杂输出添加快照测试
- 提升覆盖率的机会
有效模式(不要标记)
- 测试中的
unwrap()/expect()— 遇到意外错误时触发 panic 是正确的测试行为 - 测试模块中的
use super::*— 这是访问父级条目的标准模式 - 测试辅助函数上的
#[allow(dead_code)]— 并非每项测试都会用到这些辅助函数 - 测试中的
clone()— 清晰度优先于性能 - 较长的测试函数 — 集成测试可以很长;提取辅助函数不一定更清晰
- 用于布尔值检查的
assert!— 表达式明显是布尔值时(如.is_some()、.is_empty()),这样写没问题 - 用于测试同一逻辑行为的多个断言 — 有时一个行为需要多项检查
- 返回
Result的测试函数中使用unwrap()— 使用?传播错误也可以,但不是必须的 - 需要
dyn分发的 mock trait 上使用async-trait— trait 中的原生async fn不支持dyn Trait;此时仍需要async-trait - 测试辅助函数上带有理由说明的
#[expect]— 在测试代码中使用可自动清理的 lint 抑制是正确做法 - 昂贵的共享测试 fixture 使用
LazyLock— 对测试全局变量进行线程安全的惰性初始化是合适的
提交发现前
报告任何问题前,请加载并遵循 review-verification-protocol 技能。
评论
支持 Markdown