首页 / Rust 入门教程 / 集成测试

Rust 入门教程

集成测试

本教程共 78 篇 · 第 53 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

RustRust 入门教程集成测试tests目录cargo testlib包私有函数测试

本节目标:理解单元测试和集成测试的区别,学会把集成测试写进 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.rssrc/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 设计该反思了。好的集成测试往往顺带提升了库的可用性。

下一章我们把测试放大看:如何组织大量测试、怎么控制它们的运行。