这一节为《Rust 程序设计语言》第 11.3 节 测试的组织结构主要阐述了 Rust 社区如何组织与管理代码测试。
核心内容围绕 单元测试(Unit Tests) 和 集成测试(Integration Tests) 的设计差异、目录结构及运行机制展开。
1. 单元测试(Unit Tests)
-
定位 :更小、更集中,在隔离环境中验证单个模块的功能,可直接测试私有接口。
-
文件位置 :与被测代码放在同一文件内(通常位于
src/下)。 -
组织形式:
-
在文件底部创建内部模块,通常命名为
mod tests。 -
标注
#[cfg(test)]:告诉编译器仅在运行cargo test时才编译和运行该模块,在cargo build构建生产包时完全忽略,从而节省编译时间并减小二进制体积。 -
模块内部通常通过
use super::*;将父模块的项引入作用域。 -
私有函数测试:
-
Rust 允许对私有(非
pub)函数进行直接测试。因为测试模块tests本质上是该文件的子模块,子模块天生有权访问祖先模块中的所有私有项。
2. 集成测试(Integration Tests)
-
定位 :对被测库而言是完全外部的,模拟第三方用户调用公共 API 的场景,用于验证多个模块组合在一起能否正常协同工作。
-
文件位置与规则:
-
存放在项目根目录下的
tests/目录(与src/同级)。 -
Cargo 会特殊对待
tests/目录:**无需标注#[cfg(test)]**,仅在cargo test时才会编译。 -
tests/目录下的每个.rs文件都会被作为一个完全独立的 crate 来编译。 -
因此,测试文件顶部必须显式通过
use <crate_name>::...;引入库的公有 API。 -
按需运行:
-
运行所有测试:
cargo test -
仅运行某个集成测试文件中的所有测试:
cargo test --test <file_name>(例如cargo test --test integration_test)。 -
执行顺序与中断机制:
-
cargo test依次执行:单元测试 → 集成测试 → 文档测试。 -
如果前一步有测试失败,后续阶段将被直接跳过。
3. 集成测试中的共享子模块(避免输出杂音)
- 问题 :如果在
tests/下直接新建辅助文件(如tests/common.rs),Cargo 会把它当作独立的测试 crate 编译,并在运行测试时多打印一行Running tests/common.rs ... running 0 tests。 - 解决方案:
- 使用 Rust 旧式的模块文件路径:创建
tests/common/mod.rs。 - Cargo 识别到这种目录结构后,不会 把
common/目录下的内容当成单独的测试 crate。 - 在其他集成测试文件中,只需写
mod common;即可将辅助代码/公共配置(如common::setup())导入使用。
4. 二进制 Crate(Binary Crate)的集成测试
- 限制 :只有库 crate(Library Crate,即
src/lib.rs) 才会向外暴露公有接口供集成测试调用。如果项目只有src/main.rs(纯二进制 crate),无法在tests/目录下通过use引入并进行集成测试。 - 业界标准模式:
- 将绝大部分核心业务逻辑拆分到
src/lib.rs中。 src/main.rs仅保留一个极其轻量的外壳,负责参数解析并调用src/lib.rs提供的公共函数。- 这样既能对
src/lib.rs进行充分的集成测试,又能保证二进制程序的稳定。
总结对比
| 维度 | 单元测试 (Unit Tests) | 集成测试 (Integration Tests) |
|---|---|---|
| 存放路径 | src/ 源码内部(各代码文件底部) |
根目录 tests/*.rs |
| 条件编译 | 需要显式标注 #[cfg(test)] |
自动识别,无需标注 |
| 测试目标 | 单个函数/模块,可测试私有接口 | 跨模块流程,仅可测试公有 API |
| 编译形式 | 与当前 crate 一起按测试模式编译 | 每个文件独立编译为一个外部 crate |
| 公用代码 | 模块内辅助函数或 super::* |
存放在 tests/<module>/mod.rs |