Rust 测试:从单元测试到基于属性的测试
Rust 是少数几种语言之一,其类型系统和所有权机制在编译阶段就能排除整类错误(数据竞争、悬垂指针、双重释放内存)。然而,逻辑错误、边界情况处理不当以及错误的业务逻辑仍然存在。为了发现这些问题,Rust 生态系统形成了一套强大的测试工具集——从内置的测试运行器到用于生成测试数据的高级框架。
内置工具:cargo test
基本的测试工具直接内置于编译器和包管理器中。要创建测试,只需添加一个带有 #[cfg(test)] 属性的模块,并用 #[test] 属性标记函数即可。
// src/lib.rspub fn add(left: u64, right: u64) -> u64 { left + right}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_add() { let result = add(2, 2); assert_eq!(result, 4); }
#[test] fn test_add_negative() { // u64 不支持负数——这是编译错误 // 但如果函数接受 i64: // assert_eq!(add(-2, 2), 0); }}运行:cargo test。标志 -- --nocapture 会输出测试中的 println!。标志 -- --test-threads=1 会禁用并行运行。
集成测试
集成测试位于项目根目录的 tests/ 文件夹中。每个文件都是一个独立的 crate,可以测试库的公共 API。
// tests/integration_test.rsuse my_library::add;
#[test]fn test_integration() { assert_eq!(add(10, 20), 30);}对于测试二进制 crate(带有 main.rs),可以使用 assert_cmd——它将二进制文件作为外部进程运行并检查 stdout/stderr。
基于属性的测试:proptest 和 quickcheck
与其手动编写数十个测试用例,基于属性的测试会生成数千个随机输入数据并检查不变量。两个主要框架:
- proptest——现代、灵活,支持策略、过滤和示例缩减(shrink)。
- quickcheck——较老,受 Haskell QuickCheck 启发,更简单但功能较弱。
使用 proptest 的示例:
use proptest::prelude::*;
fn reverse_string(s: &str) -> String { s.chars().rev().collect()}
proptest! { #[test] fn test_reverse_twice(s in ".*") { let reversed = reverse_string(&s); let double_reversed = reverse_string(&reversed); assert_eq!(s, double_reversed); }
#[test] fn test_reverse_length(s in ".*") { let reversed = reverse_string(&s); assert_eq!(s.len(), reversed.len()); }}如果测试失败,proptest 会找到最小的反例(shrinking)。
测试异步代码:tokio-test
对于使用 tokio 的异步函数,普通测试不适用——需要一个异步运行时。tokio-test 提供了 #[tokio::test] 宏。
use tokio::time::{sleep, Duration};
async fn delayed_add(a: u32, b: u32) -> u32 { sleep(Duration::from_millis(10)).await; a + b}
#[tokio::test]async fn test_delayed_add() { let result = delayed_add(3, 5).await; assert_eq!(result, 8);}也可以使用 futures-test 来处理 futures 和 async-std。
模拟和桩:mockall 和 mockito
为了将测试与外部依赖(数据库、API、文件系统)隔离开来,