Rust最初是以可靠性和安全性为重点设计的,因此模块测试成为语言生态系统中不可或缺的一部分。测试在语言和工具链层面(cargo test)集成,使其成为开发过程中的有机部分。
问题的历史
在传统生态系统中(例如C/C++、Python、Java),测试往往是与程序分开的。在Rust中,测试是代码的一部分,它们被编译并作为完整的模块进行验证。这种协同作用通过语言构造和编译器的特性实现。
问题
如果没有正确的测试,就无法保证关键功能的可靠性。对于复杂和多模块的项目,常常会出现一个问题:如何方便地组织测试,使其不依赖于其他模块的状态并不复杂化项目结构?
解决方案
在Rust中,测试可以放置在源文件内部(使用#[cfg(test)]),或在单独的tests文件夹中进行集成测试。每个模块可以添加私有测试,这些测试可以访问私有API。
代码示例:
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }
关键特性:
use super::*; 在测试模块中有什么用?
为了使测试能够访问当前模块的函数和结构(包括私有),通常在测试中使用 use super::*;。
#[test] 能否是异步的?
在语言中,默认情况下#[test]不支持async,但通过外部crate(例如tokio或async-std)可以实现async #[test]。
代码示例:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
测试可以更改全局状态吗?如何避免?
Rust中的测试默认是并行执行的,因此不能使用未同步的共享全局状态(static mut),否则会出现竞态。
所有测试使用一个全局变量:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
优点:
缺点:
每个测试都是隔离的,使用局部变量和mock对象。
优点:
缺点: