如何共享测试夹具?
解读
在国内 Rust 岗位面试中,“共享测试夹具”不仅考察候选人对 Cargo 测试框架的熟悉度,更关注跨模块复用、编译速度、数据隔离三大痛点。面试官希望听到:
- 如何只编译一次就能被所有测试用例复用;
- 如何避免全局可变状态导致用例互相污染;
- 如何在集成测试、文档测试、基准测试三种场景下统一复用同一套夹具。
若仅回答“把代码放 common 模块”会被追问生命周期、并发安全及 CI 缓存细节,因此必须给出可落地的工程方案。
知识点
- Cargo 测试分类:单元测试(src 内)、集成测试(tests/)、基准测试(benches/)、示例(examples/)。
- 测试夹具三要素:构建逻辑、不可变快照、可变上下文。
- Rust 的链接规则:tests/ 目录下的每个文件都是独立 crate,无法直接引用兄弟文件符号,必须通过“tests/common/mod.rs”或独立辅助包解决。
- once_cell / std::sync::OnceLock:实现进程级一次性初始化,避免重复构造。
- #[fixture] 模式:借鉴 rstest 等第三方 crate,用属性宏生成参数化用例,编译期展开,零运行时开销。
- #[serial] / 文件锁:在缺乏硬件隔离的 CI 容器里,防止端口/数据库等全局资源冲突。
- dev-dependencies 可见性:仅在测试编译阶段引入,不会污染生产二进制。
答案
推荐“三件套”方案,兼顾速度与隔离,可直接写进简历项目经验。
-
创建 tests/common 迷你包
在项目根目录新建tests/common/mod.rs,声明公有函数与结构体;
在同一目录添加tests/common/Cargo.toml(name = “test-common”),把本项目作为 path 依赖引入;
这样集成测试、基准测试、examples 三处都能use test_common::*,且 Cargo 会把 common 编译为独立 rlib,增量编译时只构建一次。 -
用 OnceLock 做“零成本全局夹具”
// tests/common/src/fixture.rs use std::sync::OnceLock; static DB: OnceLock<Database> = OnceLock::new(); pub fn db() -> &'static Database { DB.get_or_init(|| { let db = Database::new("postgres://test:123@localhost:5432/test"); db.run_migrations().unwrap(); db }) }所有测试用例通过
common::db()拿到不可变引用,既共享连接池,又避免重复迁移;若需要可变状态,则返回Arc<Mutex<>>并在用例里显式加锁,符合 Rust 并发模型。 -
单元测试内用 #[cfg(test)] 模块
在 src 目录下创建src/test_fixture.rs,顶部加#[cfg(test)],把公共构建函数放进来;
单元测试#[cfg(test)] mod tests { use crate::test_fixture::*; }可直接复用,不会编译进 release。 -
参数化与隔离
对需要多组数据的场景,使用rstest = "0.18"作为 dev-dependency:#[rstest] #[case::alice("alice", 18)] #[case::bob("bob", 20)] fn age_limit(#[from(common::fixture::user)] u: User, #[case] name: &str, #[case] age: u8) { assert_eq!(u.name, name); assert_eq!(u.age, age); }rstest 在编译期展开成多个独立函数,互不影响,且无需手写 for 循环。
-
CI 缓存技巧
在 GitHub Actions 或 Gitee Go 里把target/debug/deps/libtest_common-*.rlib路径加入缓存 key;
当 common 代码不变时,后续 Job 直接复用,可把“cargo nextest”耗时从 3 min 降到 30 s,国内面试官非常看重这种工程提效细节。
拓展思考
- 多crate 工作区场景:把 test-common 提升到 workspace 根目录,作为独立 member,所有子 crate 的测试统一引用,避免重复编译;
- 嵌入式无 std 环境:使用
static mut+critical_section::with()实现单例夹具,同样遵循“构造一次、共享只读”原则; - 数据库回滚策略:在夹具层封装
TestTransaction,用DROP OWNED BY CURRENT_USER或 SQLite:memory:实现用例级隔离,解决并行测试脏数据问题; - 与 property-based testing 结合:利用 proptest 的
TestRunner::new(Config::with_cases(1000)),把 OnceLock 生成的复杂结构作为种子输入,在 CI nightly 管道做随机化压力测试,体现质量意识。