测试组织与运行控制
本教程共 78 篇 · 第 54 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:掌握
cargo test的各种运行控制手段——区分参数位置、切换并行与串行、查看被吞掉的打印、按需过滤测试和忽略慢测试,并学会引入只在测试时用的依赖。
测试少的时候 cargo test 一把梭就行。等项目变大、测试成百上千,每次全跑不现实,你也受不了。这章讲怎么精细地控制测试的运行。
1-1 两类命令行参数:— 是分界
cargo test 背后做的事和 cargo run 类似:先把测试编译成一个可执行文件,再运行它,跑完删掉。因为它本质是”运行一个程序”,所以能接收两类参数,中间用 -- 隔开:
--前面的参数,交给cargo test命令本身。--后面的参数,交给编译出来的那个测试二进制。
查看帮助就能看出来:
cargo test --help # 看 cargo test 自己的参数
cargo test -- --help # 看测试二进制支持的参数
初学容易把这两类参数混在一起写,结果要么报错要么参数没生效。记住:-- 之后才是控制”测试怎么跑”的。
1-2 并行还是串行
默认情况下,每个测试函数都开一个独立线程并行跑,主线程等它们全部结束收结果。并行最大的好处是快——几百个测试一起跑,总时间约等于最慢那个,而不是逐个相加。
但并行有副作用:如果多个测试共享同一份外部状态(比如都往同一个文件写数据、都连同一个数据库),谁先谁后就不可控了,A 刚写进去的数据可能被 B 覆盖,导致 A 读到错误内容而失败。这种失败不是代码错,是测试互相踩踏,最让人头疼。
Warning如果你的测试依赖共享的可变状态(文件、网络端口、全局变量),要么让每个测试用自己独占的资源,要么强制串行。否则你会遇到”单独跑就过、一起跑就挂”的灵异现象。
强制串行,把线程数设成 1:
cargo test -- --test-threads=1
参数在 -- 之后,交给测试二进制,所以是 --test-threads=1。当然也能设成 4、8,但想纯粹串行就必须是 1。
1-3 看被吞掉的打印
测试里写 println! 调试很常见。但默认规则是:测试通过了,它的标准输出不显示。只有失败的测试,它的输出才会被打出来,帮你排查。
看这段:
fn prints_and_returns_10(a: i32) -> i32 {
println!("收到的值 {}", a);
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(10, value);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(5, value);
}
}
运行后,this_test_will_pass 过了,它打印的”收到的值 4”不会出现;只有失败的 this_test_will_fail 的输出会被展示。
想不管过不过都把输出全打印出来,加 --show-output:
cargo test -- --show-output
Tip调试阶段用
--show-output很香。但提交代码前最好别依赖满屏打印,断言信息本身才该是测试失败时的主要线索。
1-4 按名称过滤测试
只跑一部分测试,最直接的是把函数名当参数传:
cargo test one_hundred
这样只有名字匹配 one_hundred 的测试会跑,其余被过滤掉(输出里用 filtered out 标出数量)。注意不能一次传多个名字,cargo test a b 不会同时跑 a 和 b,它把整串当成一个过滤词。
想按”包含某段字符”过滤,直接传片段即可:
cargo test add # 跑所有名字里带 add 的
cargo test and # 跑所有名字里带 and 的
甚至连模块名都能用来过滤:
cargo test tests # 跑 tests 模块下的全部测试
这个特性在大型项目里很实用:你只改了某个模块,就只跑那个模块的测试,省时省力。
1-5 用 #[ignore] 跳过慢测试
有些测试特别耗时(比如压测、下载大文件),日常不想每次都跑。用 #[ignore] 标注它:
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 这里要跑几十秒甚至几分钟
}
cargo test 默认跳过它,输出里显示 ignored。等你确实想验证它,单独跑被忽略的:
cargo test -- --ignored
还能组合使用,比如只跑模块里被忽略的、名字带 run 的:
cargo test run -- --ignored
Note
#[ignore]是”默认不跑但保留随时能跑”的好办法。和”直接删掉测试”完全不同——删了就没人知道那段逻辑曾被保护过。
1-6 只在测试时用的依赖
和 Node 的 devDependencies 类似,Rust 也能引入只在测试时需要的依赖。比如 pretty_assertions,它能让 assert_eq! 的失败对比更直观(带颜色、标出差异位置)。
在 Cargo.toml 里这样加:
[dev-dependencies]
pretty_assertions = "1"
然后在测试模块里覆盖引入:
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
use pretty_assertions::assert_eq; 只在 tests 模块里生效。如果在正常业务代码里这么写,编译器会报”这个包不在正常依赖里”。这正好保证了测试专用依赖不会溜进发布版本。
1-7 生成可分享的测试二进制
cargo test 其实会在 target/debug/deps/ 下生成一个测试可执行文件。你可以直接运行它,效果等同于 cargo test:
target/debug/deps/adder-92948b65e88960b4
只想编译出这个文件、不想看它立刻跑,用:
cargo test --no-run
这在你想把测试二进制打包分发给别人、或丢到 CI 机器上单独跑时很有用。
Tip真要上手持续集成(GitHub Actions 自动跑测试),把
cargo test丢进流水线就行。测试控制这块吃得透,CI 配置写起来毫无压力。
1-8 运行控制的使用心法
测试一旦变多,你会发现”只跑相关测试”是日常最高频的操作。一个实用技巧:给测试函数起有规律的名字,比如 test_add_positive、test_add_overflow,然后用 cargo test add 一次筛出所有和 add 相关的,定位问题快得多。
还有个排查卡死测试的小招:如果 cargo test 一直不结束,可能是某个测试里死循环,或者并行测试互相死锁。这时加 -- --test-threads=1 串行跑,往往能更快暴露是哪个测试卡住。配合 -- --show-output,还能看到卡住前它打印了什么。
TipCI 里常见两段式:先
cargo test --workspace跑全部,再单独cargo test -- --ignored跑慢测试。日常本地你几乎只跑过滤后的小部分,省下的时间都是自己的。
最后提醒:测试二进制本身也受 profile 影响。cargo test 用的是 test profile(继承自 dev,优化低、含溢出检查),所以测试跑得比 release 慢但更安全——溢出之类的问题在测试里更容易被抓到。别为了测试快就去改 test profile 的 overflow-checks,那等于主动关掉一道保护。
测试这块到这就讲完了。从下一章起,我们转向 Cargo 的进阶用法。