编程语言研究书库

自动化测试:测试即代码 — 第 11 章拆解

自动化测试:测试即代码

这一章讲什么: Rust 的测试机制——没有测试框架、没有断言库,测试是加了 #[test] 属性的 普通函数,"失败"就是 panic。读完你会写断言(包括"期待它 panic"这种反向断言)、 会控制测试怎么跑(并行、过滤、忽略)、会按 Rust 的约定组织单元测试和集成测试。 书开头引了 Dijkstra:测试能证明 bug 存在,不能证明 bug 不存在——所以别指望它替代类型系统1。

1. 顶层全景:最小骨架

cargo new adder --lib 自动生成的模板,就是测试的完整最小形态:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

三件装备各管一件事:#[cfg(test)] 让这段代码只在 cargo test 时编译(cfg = configuration,按编译配置条件包含;平时 cargo build 不带走,省编译时间和产物体积);#[test] 把函数标记为测试(同一个模块里可以既有测试又有普通辅助函数);cargo test 编译一个测试运行器,跑所有带标记的函数并汇报每个的成败2。

失败机制:每个测试跑在独立线程里,测试函数 panic 即失败。 所以最原始的失败方式就是 panic!("Make this test fail");输出里 failures: 段列出每个失败的原因和测试名,可按名字单独重跑调试3。

2. 断言宏:四种武器

宏用途失败时
assert!(cond)条件为真才过只说"assertion failed",不显示值
assert_eq!(a, b)相等才过打印 left/right 两个值
assert_ne!(a, b)不等才过同上
三者共用必填参数后可加格式串参数自定义失败消息

assert_eq! 比"assert! 包 =="多走一步:失败时把左右两边的实际值打印出来——调试时知道"是 5 不是 4"和只知道"不相等"完全是两种体验。参数顺序无所谓(Rust 不叫 expected/actual,就叫 left/right)。底层用 == / !=,所以参与比较的类型必须实现 PartialEq 和 Debug——自定义结构体记得 #[derive(PartialEq, Debug)]4。

主线例子(Rectangle::can_hold)演示了测试的典型节奏: 先写"大矩形装得下小矩形"的正向断言;再写反向(assert!(!smaller.can_hold(&larger)));然后故意把实现里的 > 改成 <,看测试变红——测试的价值不在绿,在于它能变红5。

自定义失败消息:必填参数后面追加格式串参数(走 format!)。例:assert!(result.contains("Carol"), "Greeting did not contain name, value was {result}")——失败时直接看到实际值6。

#[should_panic]:反向断言——"这段代码必须 panic"。 测 Guess::new 对 200 报错:测试函数体 Guess::new(200),加了 should_panic 后"panic 了 = 过、没 panic = 失败"。它有个精度问题:不管因为什么 panic 都算过;补救是给 expected 参数(#[should_panic(expected = "less than or equal to 100")]),测试框架会核对 panic 消息包含该子串7。

测试也可以返回 Result:fn it_works() -> Result<(), String> 里用 ? 传播,任一步骤 Err 即测试失败。代价:不能配 should_panic;要断言"应该失败"就用 assert!(value.is_err())8。

3. 控制测试怎么跑

cargo test 的参数分两路:-- 之前给 cargo,之后给测试二进制(cargo test -- --help 看后者)。

  • 并行:默认多线程并行跑。约束:测试之间不得共享状态——书里的反例:两个测试都读写同一个 test-output.txt,并行时互相踩数据,错的是测试不是代码。要串行:cargo test -- --test-threads=19。
  • 输出捕获:通过的测试里 println! 的输出默认被吞掉,失败测试的 stdout 会随失败信息一起显示;--show-output 全显示10。
  • 按名过滤:cargo test one_hundred 只跑这个测试;cargo test add 跑名字含 add 的所有测试(模块名也算名字的一部分,所以按模块名过滤可以跑整个模块)。一次只能给一个过滤词11。
  • #[ignore]:标在费时测试上,日常 cargo test 跳过;cargo test -- --ignored 只跑被忽略的,--include-ignored 全跑12。

4. 组织:单元测试与集成测试

单元测试:与被测代码同住 src,惯例是每个文件内开一个 mod tests + #[cfg(test)] + use super::*;。两个特性:可以测私有函数(子模块本来就看得见父模块的私有项——第 7 章隐私规则的直接推论;"私有函数该不该测"是测试学界的争论,Rust 把选择权留给你)13。

集成测试:项目顶层建 tests/ 目录,每个文件被 Cargo 编译成独立的 crate。因此它只能用公共 API——这就是它的意义:像外部用户一样使用你的库,检验各部分协作。文件头要写 use adder::add_two;,且不需要 #[cfg(test)](tests/ 目录 Cargo 特殊对待,只在 cargo test 时编译)14。

cargo test 的输出分三段:单元测试、每个集成测试文件一段、Doc-tests(文档里的代码示例也会被编译运行,第 14 章细讲)。任一段失败,后面的段不再运行15。

两个实战细节:

  1. 共享辅助代码:tests/ 下每个文件都是独立 crate,直接建 tests/common.rs 装 setup 函数的话,Cargo 会把它也当测试文件(输出里多一段 "running 0 tests")。解法是用老式写法 tests/common/mod.rs——子目录里的文件不会被当成测试 crate,然后在需要的测试文件里 mod common; 引用16。
  2. 二进制 crate 没有集成测试:tests/ 里没法 use main.rs 里的函数——只有库 crate 对外暴露函数。所以惯例再次显灵:重要逻辑放 src/lib.rs,src/main.rs 只留薄壳;集成测试测库,薄壳默认它没问题17。

5. 作者的判断与证据

  • "类型系统和所有权挡得住一类 bug,但挡不住逻辑 bug——所以仍要测试"——本章的定位声明,开篇 Dijkstra 引言是同一意思1。
  • "私有函数可测,但测不测随你"——社区争论被如实呈现,作者不站队13。
  • "二进制逻辑进 lib.rs"——惯例主张,这里给出了测试维度的新理由17。

判断(我们的,不是书里的): 本章最值得注意的设计是"测试无特权":测试就是代码, 复用整套模块/privacy/cfg 机制,没有第二套系统要学。代价也真实存在—— tests/ 目录"每文件一 crate"的语义(以及 common/mod.rs 这个绕路解法)是学习曲线上的暗坑, 它本质上是模块系统的一处特例,而非新机制。 如果错,会错在: "无特权"的另一面是"无辅助":没有内建的 setup/teardown 生命周期, 共享夹具要靠 mod 手搓;对从 xUnit 传统过来的测试者,Rust 这套反而要重新学。

6. 边界与局限

  • 基准测试明确说"目前只有 nightly 可用",没有展开。
  • 测试覆盖率工具一个字没提。
  • "怎么写好测试"(命名、粒度、mock)本章明示只讲机制,不讲手艺。

7. 可带走的

  1. 测试 = #[test] 函数;失败 = panic;cargo test 跑全部。
  2. #[cfg(test)] 让测试代码不进正式产物。
  3. assert! 只报失败位置;assert_eq!/ne! 打印左右值;自定义类型要 derive PartialEq + Debug。
  4. 必填参数后可加格式串,自定义失败消息。
  5. #[should_panic(expected = "...")] 测"必须 panic",expected 提高精度。
  6. 测试可返回 Result 并用 ?;但不能配 should_panic。
  7. 默认并行;共享状态的测试要么隔离要么 --test-threads=1。
  8. cargo test 名字 过滤;#[ignore] + -- --ignored 管理费时测试。
  9. 单元测试住 src(可测私有),集成测试住 tests/(只用公共 API、每文件一 crate)。
  10. 共享辅助代码放 tests/common/mod.rs;二进制 crate 测不了,逻辑放 lib.rs。

8. 原文地图

主题原书章原文位置
测试函数与 cargo testChapter 11: Writing Automated Teststext/18-ch11-chapter-11-writing-automated-tests.txt:46(搜「Structuring Test Functions」) · text/18-ch11-chapter-11-writing-automated-tests.txt:101(搜「cargo test」)
失败=panic同上text/18-ch11-chapter-11-writing-automated-tests.txt:170(搜「Tests fail when」)
assert! 与 can_hold同上text/18-ch11-chapter-11-writing-automated-tests.txt:232(搜「assert! Macro」)
assert_eq! 打印左右值同上text/18-ch11-chapter-11-writing-automated-tests.txt:436(搜「left == right」)
PartialEq + Debug 要求同上text/18-ch11-chapter-11-writing-automated-tests.txt:470(搜「debug formatting」)
自定义失败消息同上text/18-ch11-chapter-11-writing-automated-tests.txt:549(搜「Greeting did not contain name」)
should_panic 与 expected同上text/18-ch11-chapter-11-writing-automated-tests.txt:564(搜「should_panic」) · text/18-ch11-chapter-11-writing-automated-tests.txt:691(搜「expected parameter」)
测试返回 Result同上text/18-ch11-chapter-11-writing-automated-tests.txt:741(搜「Result<T, E> in Tests」)
并行与 test-threads同上text/18-ch11-chapter-11-writing-automated-tests.txt:792(搜「Parallel」)
输出捕获与 show-output同上text/18-ch11-chapter-11-writing-automated-tests.txt:819(搜「Showing Function Output」)
按名过滤与 ignore同上text/18-ch11-chapter-11-writing-automated-tests.txt:963(搜「Running Single Tests」) · text/18-ch11-chapter-11-writing-automated-tests.txt:1005(搜「Ignoring Tests」)
cfg(test) 与单元测试同上text/18-ch11-chapter-11-writing-automated-tests.txt:1084(搜「cfg(test)」)
私有函数可测同上text/18-ch11-chapter-11-writing-automated-tests.txt:1119(搜「Private Function Tests」)
集成测试与 tests 目录同上text/18-ch11-chapter-11-writing-automated-tests.txt:1155(搜「Integration Tests」)
common/mod.rs 技巧同上text/18-ch11-chapter-11-writing-automated-tests.txt:1307(搜「not what we wanted」)
二进制 crate 的限制同上text/18-ch11-chapter-11-writing-automated-tests.txt:1348(搜「Binary Crates」)

Footnotes

  1. 出处:「Chapter 11: Writing Automated Tests」第 3 段(text/18-ch11-chapter-11-writing-automated-tests.txt:3,搜「Humble Programmer」)。 ↩ ↩2

  2. 出处:「Chapter 11: Writing Automated Tests」第 46 段(text/18-ch11-chapter-11-writing-automated-tests.txt:46,搜「Structuring Test Functions」)与第 51 段(text/18-ch11-chapter-11-writing-automated-tests.txt:51,搜「test runner binary」)。 ↩

  3. 出处:「Chapter 11: Writing Automated Tests」第 170 段(text/18-ch11-chapter-11-writing-automated-tests.txt:170,搜「Tests fail when」)。 ↩

  4. 出处:「Chapter 11: Writing Automated Tests」第 436 段(text/18-ch11-chapter-11-writing-automated-tests.txt:436,搜「left == right」)与第 470 段(text/18-ch11-chapter-11-writing-automated-tests.txt:470,搜「debug formatting」)。 ↩

  5. 出处:「Chapter 11: Writing Automated Tests」第 232 段(text/18-ch11-chapter-11-writing-automated-tests.txt:232,搜「assert! Macro」)与第 372 段(text/18-ch11-chapter-11-writing-automated-tests.txt:372,搜「caught the bug」)。 ↩

  6. 出处:「Chapter 11: Writing Automated Tests」第 549 段(text/18-ch11-chapter-11-writing-automated-tests.txt:549,搜「Greeting did not contain name」)。 ↩

  7. 出处:「Chapter 11: Writing Automated Tests」第 564 段(text/18-ch11-chapter-11-writing-automated-tests.txt:564,搜「should_panic」)与第 691 段(text/18-ch11-chapter-11-writing-automated-tests.txt:691,搜「expected parameter」)。 ↩

  8. 出处:「Chapter 11: Writing Automated Tests」第 741 段(text/18-ch11-chapter-11-writing-automated-tests.txt:741,搜「Result<T, E> in Tests」)。 ↩

  9. 出处:「Chapter 11: Writing Automated Tests」第 792 段(text/18-ch11-chapter-11-writing-automated-tests.txt:792,搜「Parallel」)与第 812 段(text/18-ch11-chapter-11-writing-automated-tests.txt:812,搜「test-threads」)。 ↩

  10. 出处:「Chapter 11: Writing Automated Tests」第 819 段(text/18-ch11-chapter-11-writing-automated-tests.txt:819,搜「Showing Function Output」)。 ↩

  11. 出处:「Chapter 11: Writing Automated Tests」第 963 段(text/18-ch11-chapter-11-writing-automated-tests.txt:963,搜「Running Single Tests」)。 ↩

  12. 出处:「Chapter 11: Writing Automated Tests」第 1005 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1005,搜「Ignoring Tests」)。 ↩

  13. 出处:「Chapter 11: Writing Automated Tests」第 1084 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1084,搜「cfg(test)」)与第 1119 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1119,搜「Private Function Tests」)。 ↩ ↩2

  14. 出处:「Chapter 11: Writing Automated Tests」第 1155 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1155,搜「Integration Tests」)与第 1163 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1163,搜「tests directory」)。 ↩

  15. 出处:「Chapter 11: Writing Automated Tests」第 1226 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1226,搜「sections will not be run」)。 ↩

  16. 出处:「Chapter 11: Writing Automated Tests」第 1307 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1307,搜「not what we wanted」)。 ↩

  17. 出处:「Chapter 11: Writing Automated Tests」第 1348 段(text/18-ch11-chapter-11-writing-automated-tests.txt:1348,搜「Binary Crates」)。 ↩ ↩2

On this page