集成测试
本教程共 78 篇 · 第 53 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解单元测试和集成测试的区别,学会把集成测试写进
tests/目录,知道它只能测pub的 API、如何共享辅助代码,以及为什么二进制包要拆出lib包才能被测。
单元测试盯着”一个函数对不对”,集成测试关心”几个功能拼起来对不对”。局部没问题,组合起来可能出问题,所以集成测试必不可少。Rust 给这两种测试设计了不同的组织方式。
1-1 单元测试:和代码住在一起
先回顾单元测试的惯例。被测代码和测试写在同一个文件里,靠 #[cfg(test)] 标记:
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(add_two(2), 4);
}
}
#[cfg(test)] 是条件编译标记。cfg 是 configuration(配置)的缩写,意思是”只有当 test 这个配置存在时,才编译下面的模块”。而 cargo test 运行时会带上 test 配置,于是这个 tests 模块被编进去;cargo build 时没有 test 配置,它就像空气一样被忽略掉。
Note条件编译带来两个好处:一是省编译时间,正常构建不用编译测试代码;二是减小最终二进制文件的体积。测试代码不会混进你发布给用户的可执行文件里。
单元测试有个独门绝技:能直接测私有函数。因为测试模块是父模块的子模块,用 use super::*; 就能把父模块的私有项拉进作用域:
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
internal_adder 没有 pub,平时外部调不到,但测试模块里照样能测。社区对”该不该测私有函数”一直有争论,Rust 的态度是:你行你上,语言层面完全支持。
1-2 集成测试:单独住一个目录
集成测试不住在被测代码旁边,而是住在项目根目录下的 tests/ 文件夹(和 src/ 同级,一般要手动建)。Cargo 会自动去这个目录里找测试文件,对每个文件单独编译成一个包来跑。
新建 tests/integration_test.rs:
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
注意和单元测试的三点不同:
- 这里没有
mod tests,也没有#[cfg(test)]。 tests目录下的每个文件都是一个独立的包,所以得先用use adder;把要测的包引入当前作用域。项目名是adder,Cargo 自动生成的库包名也是adder,二者对应。- 因为集成测试是”外部用户”的视角,它只能调用
pub暴露出来的 API,测不到私有函数。
运行 cargo test,输出会分成三段:先是 Running unittests(单元测试),再是 Running tests/integration_test.rs(集成测试),最后是 Doc-tests(文档测试)。
只想跑某个集成测试文件,用 --test 指定文件名(不含后缀):
cargo test --test integration_test
这条命令只跑 integration_test.rs,单元测试和文档测试都不动。
Tip集成测试文件彼此独立,互不共享。想多写几个测试,要么往同一个文件里加
#[test]函数,要么再建一个新文件。每个文件编译成一个独立二进制,互相之间是隔离的。
1-3 共享辅助模块
多个集成测试文件可能都要用同一段准备代码,比如一个 setup() 初始化函数。新手容易直接建 tests/common.rs,结果它也被当成测试文件编译运行了,输出里凭空多一段 running 0 tests,很碍眼。
正确做法是建 tests/common/mod.rs:
pub fn setup() {
// 初始化一些测试状态
}
然后用子目录的方式组织,Rust 就不会把它当独立测试包。在其它集成测试文件里这样用:
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
记住一条规律:tests 目录下的子目录里的文件不会被当成独立测试包,也不会产生测试输出。把共享代码放子目录里,干净利落。
1-4 二进制包不能直接测
有个重要限制:Rust 目前只支持对库包(src/lib.rs)做集成测试,对二进制包(src/main.rs)不行。原因是你没法用 use 引入一个二进制包——只有库包能被别的包引入。
这正是为什么成熟的 Rust 项目常常同时有 src/main.rs 和 src/lib.rs 两个包:main.rs 只保留”读参数、调库、打印”的主干,真正的业务逻辑全塞进 lib.rs。这样业务逻辑能在 lib 里被单元测试和集成测试充分覆盖;而 main.rs 主干足够简单,集成测试一通,主干基本也就没问题了。
Warning如果你克隆了一个只有
main.rs的老项目想补测试,会发现tests/下怎么写都引入不了代码。别硬刚,先把核心逻辑挪到lib.rs,再让main调用它。这是 Rust 社区的标准做法。
1-5 两种测试怎么选
单元测试和集成测试不是二选一,而是互补:
- 单元测试快、聚焦,能深入私有实现细节,适合验证每个小零件。
- 集成测试慢一点,但站在用户视角,能抓住”零件单独好使、拼起来出岔子”的问题。
一个实用的节奏:给每个公开函数写单元测试保底,给关键的用户流程写集成测试兜底。测试写多了你会慢慢有感觉——哪类问题值得用哪种测试去拦。
1-6 测试金字塔与日常取舍
单元测试和集成测试之外,还有”端到端测试”,但 Rust 社区最看重前两者。一个常见的失衡是:有人只写集成测试,结果每个集成测试都要把整个包编译链接一遍,跑得慢;有人只写单元测试,结果模块拼起来才暴露的 bug 抓不到。
平衡的做法是:底层函数和纯逻辑用单元测试密集覆盖,对外 API 和关键流程用少量集成测试兜底。日常你改一个函数,跑对应单元测试秒回结果;要发版前,再跑一遍全量集成测试确认没回归。记住,测试的价值不在于数量,而在于它能在你犯错时第一时间响亮地红给你看。
Note集成测试每次都独立编译成一个二进制,所以文件多了编译开销会上升。如果某些集成测试极慢(比如要起真实数据库),用本章讲的
#[ignore]标起来,只在 CI 的特定阶段才跑--ignored。
另一个容易忽视的点:集成测试是”外部视角”,它逼着你把公共 API 设计得干净好用。如果你发现某个功能在集成测试里极难调用,那通常不是测试的错,而是你的 API 设计该反思了。好的集成测试往往顺带提升了库的可用性。
下一章我们把测试放大看:如何组织大量测试、怎么控制它们的运行。