认识两个测试属性
在 Rust 项目中写测试时,#[test] 和 #[cfg(test)] 是出现频率最高的两个属性。很多刚接触 Rust 的开发者会把它们混为一谈,或者不清楚各自的使用场景。实际上它们分工明确:#[test] 负责标记测试函数,#[cfg(test)] 负责控制测试代码的编译条件。
在 Rust 中,通过 #[test] 属性标记的函数会被识别为测试用例。运行 cargo test 时,编译器会编译所有标记了 #[test] 的函数,并在测试 runner 中执行它们。需要注意的是,#[test] 只是告诉编译器这是一个测试函数,并不影响源码的常规编译——在非测试模式下,这些函数会被忽略。因此,你可以在同一个文件中混合编写业务代码和测试代码,而不会增加生产二进制文件的体积。
但光是标记 #[test] 还不够——测试中经常需要导入额外的依赖、定义辅助函数或 mock 结构体,这些代码如果直接写在文件顶层,即使加了 #[test] 也会在正常编译时被处理(尽管不会被调用但会增加编译产物体积)。这时候就需要 #[cfg(test)] 来帮忙。
条件编译的作用
#[cfg(test)] 是一个条件编译属性,指示编译器仅在 cfg=test 配置下编译其后附带的项。通常,它会包裹整个测试模块(如 mod tests),确保测试相关的辅助函数、 mock 结构体、导入等只在测试构建时存在。这样做可以避免测试代码泄露到生产环境中,同时允许你在测试模块中自由编写辅助函数而无需担心命名冲突或额外编译开销。
简单理解:#[cfg(test)] 是给编译器看的开关——只有当你使用 cargo test 时,编译器才会把被这个属性包裹的代码纳入编译范围。如果你用 cargo build,这些代码就像不存在一样。这是 Rust 条件编译系统和测试框架配合精妙的地方。
标准模式:模块内测试
Rust 社区推荐将单元测试写在每个源文件的底部,并用 #[cfg(test)] 包裹一个 tests 模块。在该模块内部,使用 use super::* 导入外部项,再用 #[test] 标记每个测试函数。这种模式的好处是:测试代码仅在运行 cargo test 时编译,生产构建完全忽略它们;同时测试模块可以访问父模块的私有函数,方便进行白盒测试。注意,如果你在模块外直接写 #[test] 函数而不加 #[cfg(test)],该函数仍会被编译但不会在非测试运行时执行,不过会增加编译目标体积。
具体操作时,通常在文件末尾这样写:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}执行 cargo test 后,测试会自动发现并运行。如果执行 cargo build,那么 mod tests 整个模块都不会被编译,生产二进制中不包含任何测试相关的符号。这种隔离机制对保持生产包体积和安全性都有帮助。
常见陷阱:跨模块引用测试代码
一个常见错误是在非测试代码中通过 use crate::module::tests::helper 引用测试辅助函数。由于 tests 模块被 #[cfg(test)] 包裹,在正常构建时该模块根本不存在,导致编译错误。正确的做法是保持测试代码完全隔离,只通过父模块的公有接口进行测试。如果需要共享辅助函数,应将它们定义在 pub(crate) 的普通模块中,并用条件编译控制其包含的逻辑。
举个例子:你写了一个 helper 函数只在测试时需要,那可以单独创建一个 test_utils.rs 文件并用 #[cfg(test)] 包裹其整个模块,然后在测试模块中通过 use crate::test_utils::helper 引用。但绝对不要在业务代码中 import 这个模块,否则编译会失败。
集成测试 vs 单元测试的配置差异
对于集成测试(放在 tests/ 目录下的 .rs 文件),每个文件是一个独立的 crate,不需要使用 #[cfg(test)] 属性——因为整个文件仅在测试运行期间被编译。而单元测试(放在 src/ 下的文件中)必须用 #[cfg(test)] 包裹模块,否则测试代码会随业务代码一起编译。另外,集成测试中无法直接测试私有函数,只能通过 crate 的公有 API 进行黑盒测试,这一点与单元测试形成对比。
因此判断依据很清晰:如果测试代码和业务代码在同一个文件中,必须用 #[cfg(test)] 包裹;如果测试代码放在独立的 tests/ 目录,那整个文件天然就是仅测试环境编译,不需要再加条件编译属性。不过集成测试文件里仍然需要用 #[test] 标记每个测试函数。
总结判断思路
问自己几个问题就能快速决策:
- 这段代码是测试函数吗?→ 加
#[test]。 - 这段代码是否只应在测试时存在(比如辅助函数、mock 结构体、额外 use 语句)?→ 用
#[cfg(test)]包裹整块。 - 这个测试函数是否放在业务源文件内?→ 必须再包一层
#[cfg(test)] mod tests { }。 - 这个测试函数是否在
tests/目录下?→ 不需要#[cfg(test)],但每个函数仍需#[test]。
只要按这个思路走,基本不会出错。