关于 rust-testing
rust-testing 是一项帮助 Rust 开发者根据要验证的行为选择测试方式的技能。它涵盖常见测试形式、黄金文件测试和移植项目的差分测试,并说明确定性与验证要求。
主要功能
- 01从五种常见测试形式中选择
- 02使用表驱动用例并提供明确失败信息
- 03验证错误变体和独立预期值
- 04保持测试确定性且不访问网络
- 05对 Rust 移植应用差分测试
适用场景
- 01为 Rust 代码变更选择测试形式
- 02为错误行为添加回归测试
- 03将 Rust 移植版本与现有实现对比
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-testing安装后在 Claude Code 中输入/rust-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
DIFFERENTIAL-TESTING.mdTEST-DESIGN.mdagents/openai.yaml详细介绍
Rust 测试
测试套件表明团队拒绝默默接受哪些变更。这项技能决定什么值得测试,以及该采用何种形式——五种常见形式、适用于文件类产物的黄金文件,以及适用于移植的差分测试;它从不决定被测代码应该是什么样子。代码的塑造属于代码实践技能;至于某项变更是否经过测试,作为审查结论,则由 /rust-code-review 决定。
测试的目的
测试的存在是为了在行为发生变化时失败。复述实现的测试——例如断言 getter 返回传入的字段值——会在代码重构时失败,却会在行为出错时通过,恰恰本末倒置。编写测试前,先用一句话说明它能捕获哪种回归。如果这句话是“代码变了”,就不要编写这个测试。
根据断言选择形式
形式应由断言决定,而不是由习惯决定。各种形式的适用深度——文件所在位置、可访问范围、成本——详见 TEST-DESIGN.md;每种形式的一句话规则如下:
| 形式 | 适用场景 |
|---|---|
#[cfg(test)] mod tests 中的单元测试 | 断言需要访问私有内容,或被测单元是边界情况值得关注的纯函数 |
tests/ 中的集成测试 | 断言针对真实调用方看到的公共 API——像使用者一样导入 crate |
| 文档测试 | 示例首先是文档;伪装成文档测试的单元测试应放在 mod tests 中 |
属性测试(proptest) | 规则适用于整个输入空间——往返、不变量、解析器/序列化器配对 |
快照测试(insta) | 输出内容较多、结构化且适合人工审查——渲染文本、生成代码、错误报告 |
由此可见:在生产环境中发现的 bug,应在该 bug 所在的层级编写单元测试,而不是编写一个碰巧覆盖到它的端到端测试。
表驱动,避免复制粘贴
一个测试、一张用例表、一个循环:
#[test]
fn parses_durations() {
let cases = [
("1s", Some(Duration::from_secs(1))),
("500ms", Some(Duration::from_millis(500))),
("", None),
("1x", None),
];
for (input, expected) in cases {
assert_eq!(parse_duration(input), expected, "input: {input:?}");
}
}
消息能体现这张表的价值:没有 "input: {input:?}",第三个用例失败时只会报告行号,无法说明是哪个用例失败。当各个用例需要有自己的测试名称和失败行时,rstest 会将同一张表展开为每个用例一个测试。
断言行为,包括失败情况
只覆盖成功路径的测试套件无法说明错误行为,因此任何错误类型变更都能通过测试。应断言调用方会匹配的错误变体,而不是 Display 字符串——该字符串不是契约。到底有哪些变体,属于 /rust-errors 的范畴。
如果测试用与被测逻辑相同的逻辑计算预期结果,就等于什么也没断言。这种情况很容易漏掉:看起来很全面——遍历输入,并用被测函数构造预期值,或者用同一公式计算一个常量。预期值应由人工编写,或来自独立来源:参考实现、已发布的测试向量、手工推算的示例。
确定性是测试的属性,不是机器的属性
为每个生成器显式设置种子;注入时间和随机性,而不是读取时钟;单元测试和集成测试中绝不访问网络。测试一旦出现偶发失败,当天就应删除或修复;容忍一个偶发失败的套件会让所有人学会重跑,而不是仔细阅读。
覆盖率是信号,不是目标
cargo llvm-cov 能找出值得测试的分支;百分比门槛只会催生触碰代码行、却没有验证行为的测试。不要追逐数字。
移植的差分测试
当 Rust 实现取代现有实现时,最有力的证据是让两者处理相同的输入并确认结果一致。三种测试框架形式——记录的语料库、并行执行、基于共享生成器的 proptest——及其权衡,以及规范化和记录差异的规则,都在 DIFFERENTIAL-TESTING.md 中。规定哪些差异可以接受的兼容性契约属于 /port-to-rust;此技能提供的是机制,而非契约。
延后处理的内容
错误变体设计属于 /rust-errors。测试 unsafe 代码需要使用 Miri,相关内容见 /unsafe-rust。异步测试的类型、时间控制,以及为什么带有真实睡眠的 #[tokio::test] 会制造不稳定测试,属于 /async-rust。loom 涵盖通过探索交错执行而非碰运气来进行的并发测试,相关内容见 /rust-concurrency。criterion 涵盖实际上属于基准测试的测试,相关内容见 /rust-performance。作为代码审查结论,判断某项更改是否需要测试,属于 /rust-code-review。
验证
cargo test --all-features
cargo test --doc
cargo clippy --all-targets --all-features # add -- -D warnings only if the repo has no lint config
Clippy 会按仓库中配置的 lint 级别运行——带有 clippy.toml 或自身 lint 属性的仓库会保留这些配置,也不会再叠加更严格的级别。
新测试在被接受前,必须先观察到它失败:注释掉修复,或反转断言,并确认失败消息指出了实际问题。未曾观察到失败的测试,可能什么都没有断言。