本节摘要:Rust 测试内建于工具链:单元测试与私有代码同住一个文件,集成测试放独立目录当外部用户来用,文档测试把示例代码变成可执行断言。本节覆盖三层的写法与运行方式,以及 should_panic 与 Result 风格测试。读完你能为任何函数建立举证档案。
单元测试:贴身查证。 与被测代码同文件,能摸到私有函数:
pub fn fine_of(loss: u32) -> u32 { loss * 2 } #[cfg(test)] // 仅测试构建编译 mod tests { use super::*; #[test] fn fine_doubles_loss() { assert_eq!(fine_of(100), 200); } #[test] #[should_panic(expected = "越界")] fn index_out_of_bounds_panics() { let v: Vec<u8> = vec![]; index_fail(&v); } }
集成测试:模拟外人。 独立测试目录下每个文件编成单独 crate,只能走公共 API——正好检验第 7.1 节的可见性设计是否合理:想测的东西拿不到,说明边界画错了。
文档测试:示例即证据。 文档注释里的代码块会被编译运行:
/// 计算罚款额。 /// /// ``` /// use court_archive::fine_of; /// assert_eq!(fine_of(50), 100); /// ``` pub fn fine_of(loss: u32) -> u32 { loss * 2 }
示例烂了 CI 就红——文档再也不会与代码脱节。这也是 Rust 生态文档质量高的机制性原因。
cargo test # 三层全跑 cargo test fine # 按名筛选 cargo test -- --nocapture # 显示打印输出 cargo test --doc # 只跑文档测试
测试函数也可以返回 Result,用 ? 代替 unwrap,失败信息更干净。

// src/lib.rs —— 单元测试与被测代码同文件 pub fn clamp01(v: f64) -> f64 { if v < 0.0 { 0.0 } else if v > 1.0 { 1.0 } else { v } } #[cfg(test)] // 编译期剔除:正式产物不含测试代码 mod tests { use super::*; // 引入父模块全部公开项 #[test] fn mid_value_passes() { assert_eq!(clamp01(0.5), 0.5); } #[test] #[should_panic(expected = "编号必须为正")] // 断言 panic 文案片段 fn reject_zero() { panic_when(0); } fn panic_when(n: u32) { assert!(n > 0, "编号必须为正"); } #[test] fn parse_returns_err() -> Result<(), std::num::ParseIntError> { let n: u32 = "12".parse()?; assert_eq!(n, 12); // 测试函数也能用 ?,Err 即失败 Ok(()) } }
// tests/integration.rs —— 集成测试:以外部使用者身份链接公开 API use mylib::clamp01; #[test] fn boundary_zero() { assert_eq!(clamp01(-3.0), 0.0); }
三类席位各管一段:同文件单元测试可以够到私有函数,tests/ 目录的集成测试只能走公开 API(等于顺带审阅 API 设计),文档里的代码块是 doc-test,cargo test 默认三者全跑。运行目标用 cargo test --lib、cargo test --test integration 精确指定。
Rust 没有内建参数化框架,标准库时代有两条正当路径:宏生成,或循环断言配合错误信息。
#[test] fn clamp_table() { let table = [(-3.0, 0.0), (0.5, 0.5), (2.0, 1.0)]; for (input, want) in table { assert_eq!(clamp01(input), want, "输入 {} 时失败", input); } }
循环版省样板,但失败即停;要全部跑完收集失败清单,用计数比对或上第三方框架。第三条路是宏批量生成 #[test] 函数,第 9 章宏一节正好给出写法。
断言选型:值相等 assert_eq!、布尔 assert!、panic 路径 should_panic、可失败流程直接返回 Result。命名建议"行为描述"而非"方法名",失败输出本身要能当 bug 报告读。测试代码里少 mock:优先用小函数与纯数据构造成本低的依赖,mock 留给真实边界(时钟、网络)。
测试文件长大后的组织问题常被忽视。单元测试模块内部建议按行为分嵌套 mod(mod parse、mod boundary),名字用完整句子(rejects_negative_id),失败输出即成 bug 报告标题。集成测试按场景分文件:tests/smoke.rs 走主流程、tests/errors.rs 专打错误路径、tests/cli.rs 覆盖参数组合——每个文件独立编译成二进制,并行执行,粒度天然按文件切分。夹具复用放到 tests/common/mod.rs(注意是 mod.rs 才不会被当作测试文件单独执行),用函数而非全局状态构造数据。
| 席位 | 位置 | 可见性 | 运行形态 |
|---|---|---|---|
| 单元测试 | src 内 + #[cfg(test)] | 可达私有 | 与代码同编译单元 |
| 集成测试 | tests/*.rs | 仅公开 API | 每文件一个测试二进制 |
| 文档测试 | /// 代码块 | 公开 API | 逐块编译执行 |
配套一条防退化纪律:修 bug 先写复现测试再修代码,测试先红后绿的次序保证修复真的覆盖了案发现场。跳过复现直接修,同一个 bug 的回归只是时间问题——这条纪律的价值与语言无关,但在 Rust 里成本最低,因为编译器已替你排除了大部分低级变量干扰。