摘要:本文系统讲解 Rust 测试与质量保证体系,涵盖单元测试与集成测试、文档测试、Mock 与测试桩、性能测试(Benchmark)、模糊测试(Fuzzing)、CI/CD 集成等核心内容。每个知识点配有完整代码示例、对比表格、实战场景及常见问题解答,帮助开发者编写健壮的 Rust 代码。
关键词:Rust、测试、单元测试、集成测试、Mock、Benchmark、Fuzzing、CI/CD、质量保证
适合人群:已掌握 Rust 基础的开发者、想提高代码质量的程序员、想建立测试体系的团队
阅读时间:约 50 分钟
版本信息:Rust 1.70+ | 兼容 Windows/macOS/Linux
文章目录
- 一、单元测试与集成测试
-
- 1.1 单元测试
- 1.2 断言宏
- 1.3 集成测试
- 1.4 测试组织
- 二、文档测试
-
- 2.1 文档测试基础
- 2.2 文档测试技巧
- 三、Mock 与测试桩
-
- 3.1 使用 mockall
- 3.2 测试桩模式
- 四、性能测试 Benchmark
-
- 4.1 使用 criterion
- 4.2 Benchmark 对比
- 五、模糊测试 Fuzzing
-
- 5.1 使用 cargo-fuzz
- 5.2 Fuzzing 适用场景
- 六、CI/CD 集成
-
- 6.1 GitHub Actions
- 6.2 CI 检查项
- 6.3 测试覆盖率
- 💡 综合实战案例
-
- 实战:完整测试体系
- ❓ 常见问题 FAQ
- 📝 学习资源与建议
-
- 学习建议
- 官方资源
- 练习平台
- 📚 参考资料
一、单元测试与集成测试
1.1 单元测试
Rust 内置测试框架,使用 #[test] 属性标记测试函数。
1// src/lib.rs 2pub fn add(a: i32, b: i32) -> i32 { 3 a + b 4} 5 6#[cfg(test)] 7mod tests { 8 use super::*; 9 10 #[test] 11 fn test_add() { 12 assert_eq!(add(2, 3), 5); 13 } 14 15 #[test] 16 fn test_add_negative() { 17 assert_eq!(add(-1, 1), 0); 18 } 19} 20
测试运行命令:
1cargo test # 运行所有测试 2cargo test test_add # 运行指定测试 3cargo test -- --nocapture # 显示输出 4
1.2 断言宏
断言宏对比:
| 宏 | 说明 | 示例 |
|---|---|---|
| assert! | 条件为真 | assert!(x > 0) |
| assert_eq! | 相等 | assert_eq!(a, b) |
| assert_ne! | 不相等 | assert_ne!(a, b) |
| panic! | 故意 panic | panic!("expected error") |
1.3 集成测试
集成测试放在 tests/ 目录下,测试公共 API。
1// tests/integration_test.rs 2use my_crate::add; 3 4#[test] 5fn test_add_integration() { 6 assert_eq!(add(10, 20), 30); 7} 8
测试类型对比:
| 类型 | 位置 | 测试范围 | 访问权限 |
|---|---|---|---|
| 单元测试 | src/ 内 | 内部实现 | 可访问私有项 |
| 集成测试 | tests/ 目录 | 公共 API | 仅公共项 |
| 文档测试 | 文档注释中 | 示例代码 | 公共项 |
1.4 测试组织
1#[cfg(test)] 2mod tests { 3 use super::*; 4 5 #[test] 6 fn it_works() { 7 let result = 2 + 2; 8 assert_eq!(result, 4); 9 } 10 11 #[test] 12 #[should_panic] 13 fn it_panics() { 14 panic!("This should panic"); 15 } 16 17 #[test] 18 #[ignore] 19 fn expensive_test() { 20 // 耗时测试,默认跳过 21 } 22} 23
测试属性对比:
| 属性 | 说明 | 示例 |
|---|---|---|
| #[test] | 标记测试函数 | #[test] |
| #[should_panic] | 期望 panic | #[should_panic] |
| #[ignore] | 跳过测试 | #[ignore] |
| #[serial] | 串行执行 | #[serial] |
二、文档测试
2.1 文档测试基础
Rust 允许在文档注释中编写可测试的代码示例。
1/// Adds two numbers. 2/// 3/// # Examples 4/// 5/// ``` 6/// use my_crate::add; 7/// 8/// assert_eq!(add(2, 3), 5); 9/// ``` 10pub fn add(a: i32, b: i32) -> i32 { 11 a + b 12} 13
文档测试运行:
1cargo test --doc # 仅运行文档测试 2cargo test # 包含文档测试 3
2.2 文档测试技巧
1/// # 隐藏设置代码 2/// 3/// ``` 4/// # use my_crate::Config; 5/// let config = Config::new(); 6/// assert!(config.is_valid()); 7/// ``` 8pub struct Config { 9 // ... 10} 11
文档测试说明:
| 技巧 | 说明 | 示例 |
|---|---|---|
| # 前缀 | 隐藏代码行 | # use my_crate::Foo; |
| should_panic | 期望 panic | /// ```should_panic |
| ignore | 跳过测试 | /// ```ignore |
| no_run | 编译但不运行 | /// ```no_run |
三、Mock 与测试桩
3.1 使用 mockall
mockall 是 Rust 流行的 Mock 框架。
Cargo.toml 依赖:
1[dev-dependencies] 2mockall = "0.11" 3
1use mockall::automock; 2 3#[automock] 4trait Database { 5 fn get_user(&self, id: u32) -> Option<String>; 6 fn save_user(&self, id: u32, name: &str) -> bool; 7} 8 9struct Service { 10 db: Box<dyn Database>, 11} 12 13impl Service { 14 fn new(db: Box<dyn Database>) -> Self { 15 Self { db } 16 } 17 18 fn get_user_name(&self, id: u32) -> Option<String> { 19 self.db.get_user(id) 20 } 21} 22 23#[cfg(test)] 24mod tests { 25 use super::*; 26 27 #[test] 28 fn test_get_user_name() { 29 let mut mock_db = MockDatabase::new(); 30 mock_db.expect_get_user() 31 .with(mockall::predicate::eq(1)) 32 .returning(|_| Some("Alice".to_string())); 33 34 let service = Service::new(Box::new(mock_db)); 35 assert_eq!(service.get_user_name(1), Some("Alice".to_string())); 36 } 37} 38
3.2 测试桩模式
1// 定义 trait 2pub trait HttpClient { 3 fn get(&self, url: &str) -> Result<String, String>; 4} 5 6// 生产实现 7pub struct RealClient; 8impl HttpClient for RealClient { 9 fn get(&self, url: &str) -> Result<String, String> { 10 // 实际 HTTP 请求 11 unimplemented!() 12 } 13} 14 15// 测试桩 16pub struct StubClient; 17impl HttpClient for StubClient { 18 fn get(&self, url: &str) -> Result<String, String> { 19 Ok("stubbed response".to_string()) 20 } 21} 22
Mock 与 Stub 对比:
| 特性 | Mock | Stub |
|---|---|---|
| 行为验证 | 验证调用次数、参数 | 仅返回固定值 |
| 复杂度 | 较高 | 较低 |
| 适用场景 | 复杂交互 | 简单依赖 |
| 维护成本 | 较高 | 较低 |
四、性能测试 Benchmark
4.1 使用 criterion
criterion 是 Rust 标准的 Benchmark 框架。
Cargo.toml 配置:
1[dev-dependencies] 2criterion = { version = "0.5", features = ["html_reports"] } 3 4[[bench]] 5name = "my_benchmark" 6harness = false 7
1// benches/my_benchmark.rs 2use criterion::{black_box, criterion_group, criterion_main, Criterion}; 3 4fn fibonacci(n: u64) -> u64 { 5 match n { 6 0 => 1, 7 1 => 1, 8 n => fibonacci(n - 1) + fibonacci(n - 2), 9 } 10} 11 12fn criterion_benchmark(c: &mut Criterion) { 13 c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); 14} 15 16criterion_group!(benches, criterion_benchmark); 17criterion_main!(benches); 18
运行 Benchmark:
1cargo bench # 运行所有 benchmark 2cargo bench -- --save-baseline # 保存基线 3
4.2 Benchmark 对比
| 框架 | 说明 | 特点 |
|---|---|---|
| criterion | 统计基准 | HTML 报告、统计分析 |
| 内置 #[bench] | 简单基准 | 需要 nightly |
| iai | 指令计数 | 不受系统负载影响 |
五、模糊测试 Fuzzing
5.1 使用 cargo-fuzz
cargo-fuzz 用于发现边界条件 bug。
安装:
1cargo install cargo-fuzz 2
初始化:
1cargo fuzz init 2
1// fuzz/fuzz_targets/fuzz_target_1.rs 2#![no_main] 3use libfuzzer_sys::fuzz_target; 4 5fuzz_target!(|data: &[u8]| { 6 if data.len() > 0 { 7 my_crate::parse(data); 8 } 9}); 10
运行 Fuzzing:
1cargo fuzz run fuzz_target_1 2
5.2 Fuzzing 适用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 解析器 | 解析未知输入 | JSON、XML 解析 |
| 序列化 | 数据格式转换 | serde 序列化 |
| 网络协议 | 处理网络数据 | HTTP 解析 |
| 加密 | 边界条件 | 加密算法 |
六、CI/CD 集成
6.1 GitHub Actions
1# .github/workflows/ci.yml 2name: CI 3 4on: 5 push: 6 branches: [ main ] 7 pull_request: 8 branches: [ main ] 9 10jobs: 11 test: 12 runs-on: ubuntu-latest 13 steps: 14 - uses: actions/checkout@v3 15 - uses: actions-rs/toolchain@v1 16 with: 17 toolchain: stable 18 - run: cargo test 19 - run: cargo clippy 20 - run: cargo fmt --check 21
6.2 CI 检查项
| 检查项 | 命令 | 说明 |
|---|---|---|
| 测试 | cargo test | 运行所有测试 |
| Clippy | cargo clippy | 代码检查 |
| 格式化 | cargo fmt --check | 格式检查 |
| 文档 | cargo doc | 文档生成 |
| 安全审计 | cargo audit | 依赖安全 |
| 覆盖率 | cargo tarpaulin | 测试覆盖率 |
6.3 测试覆盖率
1# 安装 2cargo install cargo-tarpaulin 3 4# 运行 5cargo tarpaulin --out Html 6
💡 综合实战案例
实战:完整测试体系
1// src/lib.rs 2pub struct Calculator; 3 4impl Calculator { 5 pub fn add(a: i32, b: i32) -> i32 { 6 a + b 7 } 8 9 pub fn divide(a: i32, b: i32) -> Result<i32, String> { 10 if b == 0 { 11 Err("Division by zero".to_string()) 12 } else { 13 Ok(a / b) 14 } 15 } 16} 17 18#[cfg(test)] 19mod tests { 20 use super::*; 21 22 #[test] 23 fn test_add() { 24 assert_eq!(Calculator::add(2, 3), 5); 25 } 26 27 #[test] 28 fn test_divide() { 29 assert_eq!(Calculator::divide(10, 2), Ok(5)); 30 } 31 32 #[test] 33 fn test_divide_by_zero() { 34 assert!(Calculator::divide(10, 0).is_err()); 35 } 36} 37
1// tests/integration_test.rs 2use my_crate::Calculator; 3 4#[test] 5fn test_calculator_integration() { 6 let result = Calculator::add( 7 Calculator::add(1, 2), 8 Calculator::add(3, 4) 9 ); 10 assert_eq!(result, 10); 11} 12
项目知识点:
- 单元测试(
#[test]) - 集成测试(
tests/目录) - 错误处理测试
- 断言宏使用
❓ 常见问题 FAQ
Q1:单元测试和集成测试有什么区别?
A:主要区别:
- 单元测试在
src/内,可访问私有项 - 集成测试在
tests/目录,仅测试公共 API - 单元测试测试内部实现,集成测试测试外部接口
Q2:什么时候使用 Mock?
A:以下场景推荐使用 Mock:
- 依赖外部服务(数据库、HTTP)
- 需要验证调用次数和参数
- 测试复杂交互逻辑
- 隔离测试环境
Q3:如何提高测试覆盖率?
A:提高覆盖率技巧:
- 使用
cargo tarpaulin检查覆盖率 - 编写边界条件测试
- 测试错误路径
- 使用 Fuzzing 发现遗漏
Q4:Benchmark 和测试有什么区别?
A:区别:
- 测试验证正确性,Benchmark 测量性能
- 测试使用
cargo test,Benchmark 使用cargo bench - 测试关注功能,Benchmark 关注速度
Q5:如何在 CI 中运行测试?
A:使用 GitHub Actions:
- 创建
.github/workflows/ci.yml - 配置
cargo test、cargo clippy、cargo fmt - 推送到 GitHub 自动运行
📝 学习资源与建议
学习建议
1. 测试驱动开发:先写测试,再写实现
2. 覆盖边界条件:测试正常和异常路径
3. 使用 Mock 隔离:避免依赖外部服务
4. 定期 Benchmark:监控性能变化
5. CI/CD 集成:自动化测试流程
官方资源
练习平台
📚 参考资料
《09-Rust 测试与质量保证(单元测试 + 集成测试 + Mock + Benchmark + Fuzzing + CI/CD)》 是转载文章,点击查看原文。
