mcpField
R
rust-testing

rust-testing

github.com/rewrite-rs测试与质量1656 次安装12 周前更新
代码仓库

关于 rust-testing

rust-testing 是一项帮助 Rust 开发者根据要验证的行为选择测试方式的技能。它涵盖常见测试形式、黄金文件测试和移植项目的差分测试,并说明确定性与验证要求。

主要功能

  1. 01从五种常见测试形式中选择
  2. 02使用表驱动用例并提供明确失败信息
  3. 03验证错误变体和独立预期值
  4. 04保持测试确定性且不访问网络
  5. 05对 Rust 移植应用差分测试

适用场景

  1. 01为 Rust 代码变更选择测试形式
  2. 02为错误行为添加回归测试
  3. 03将 Rust 移植版本与现有实现对比

安装

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

$npx skills add https://github.com/rewrite-rs/skills --skill rust-testing

安装后在 Claude Code 中输入/rust-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

DIFFERENTIAL-TESTING.md
TEST-DESIGN.md
agents/
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 属性的仓库会保留这些配置,也不会再叠加更严格的级别。

新测试在被接受前,必须先观察到它失败:注释掉修复,或反转断言,并确认失败消息指出了实际问题。未曾观察到失败的测试,可能什么都没有断言。

评论

支持 Markdown