目录
- 错误处理哲学
- thiserror vs anyhow 选型
- Result 组合子
- Panic 边界与自定义 Hook
- 测试金字塔
- Mockall 模拟外部依赖
- Property-based Testing
- 代码覆盖率
1. 错误处理哲学
Rust 的错误处理哲学:可恢复错误用 Result,不可恢复错误用 panic。没有异常机制,所有错误路径必须显式处理。
// 可恢复错误:文件可能不存在
fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
// 不可恢复错误:内部逻辑矛盾,应立刻终止
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("division by zero"); // 程序员的 bug
}
a / b
}
2. thiserror vs anyhow 选型
| crate | 用途 | 适用场景 |
|---|---|---|
| thiserror | 定义结构化错误类型 | 库开发(API 暴露错误) |
| anyhow | 简化错误传播 | 应用开发(快速处理错误) |
2.1 thiserror:库级错误定义
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error at line {line}: {source}")]
Parse {
line: usize,
#[source]
source: serde_json::Error,
},
#[error("Validation failed: {0}")]
Validation(String),
#[error("Not found: {resource}")]
NotFound { resource: String },
}
// 使用
fn load_data(path: &str) -> Result<Data, DataError> {
let content = std::fs::read_to_string(path)?; // 自动 from io::Error
let data: Data = serde_json::from_str(&content)
.map_err(|e| DataError::Parse { line: 42, source: e })?;
Ok(data)
}
2.2 anyhow:应用级快速错误
use anyhow::{Result, Context};
fn main() -> Result<()> {
let config = std::fs::read_to_string("config.json")
.with_context(|| "Failed to read config file")?;
let data: Config = serde_json::from_str(&config)
.context("Failed to parse config")?;
process(&data)?;
Ok(())
}
2.3 混合使用
// 库代码使用 thiserror
pub fn library_api() -> Result<Value, LibraryError> { ... }
// 应用代码使用 anyhow
fn main() -> anyhow::Result<()> {
let v = my_crate::library_api().context("library call failed")?;
Ok(())
}
3. Result 组合子
3.1 链路式处理
let user = fetch_user(id)
.map_err(|e| Error::Database(e))?
.ok_or(Error::UserNotFound)?
.validate()?
.into_dto();
3.2 map / and_then / or_else
let parsed: Result<i32, _> = "42".parse();
let doubled = parsed.map(|n| n * 2); // Ok(84)
let or_zero = parsed.unwrap_or(0); // 42
// 链式转换
let result = config
.get("timeout")
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(30);
3.3 收集多个错误
// 使用 itertools 收集所有验证错误
let errors: Vec<_> = inputs.iter()
.filter_map(|input| validate(input).err())
.collect();
if !errors.is_empty() {
return Err(Error::BatchValidation(errors));
}
4. Panic 边界与自定义 Hook
4.1 catch_unwind
use std::panic;
let result = panic::catch_unwind(|| {
may_panic()
});
match result {
Ok(val) => println!("Success: {}", val),
Err(_) => println!("Task panicked"),
}
⚠️
catch_unwind不捕获所有 panic(如abort模式),且不应作为常规错误处理。
4.2 自定义 Panic Hook
std::panic::set_hook(Box::new(|info| {
log::error!("Panic occurred: {}", info);
// 发送到 Sentry/Error Tracking
sentry::capture_event(sentry::protocol::Event {
message: Some(format!("{}", info)),
level: sentry::Level::Fatal,
..Default::default()
});
}));
4.3 不 panic 的 API 设计
// ❌ 容易 panic
pub fn get_unchecked(&self, index: usize) -> &T { &self.items[index] }
// ✅ 返回 Result
pub fn get(&self, index: usize) -> Result<&T, BoundsError> {
self.items.get(index).ok_or(BoundsError { index })
}
// ✅ 或返回 Option
pub fn try_get(&self, index: usize) -> Option<&T> {
self.items.get(index)
}
5. 测试金字塔
Rust 内置三种测试:
| 类型 | 位置 | 用途 |
|---|---|---|
| 单元测试 | 模块内 #[cfg(test)] | 测试单个函数/模块 |
| 集成测试 | tests/ 目录 | 测试公共 API 组合 |
| 文档测试 | /// 中的代码块 | 确保文档示例可运行 |
5.1 单元测试
// src/calculator.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, -3), -5);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_add_overflow() {
let _ = i32::MAX + 1; // debug 模式会 panic
}
}
5.2 集成测试
// tests/api_test.rs
use my_crate::Client;
#[tokio::test]
async fn test_create_user() {
let client = Client::new("http://localhost:3000");
let user = client.create_user("Alice").await.unwrap();
assert_eq!(user.name, "Alice");
}
5.3 文档测试
/// 计算两个数的最大公约数
/// ```
/// use my_crate::gcd;
/// assert_eq!(gcd(12, 8), 4);
/// ```
pub fn gcd(a: u64, b: u64) -> u64 {
if b == 0 { a } else { gcd(b, a % b) }
}
5.4 异步测试
#[tokio::test]
async fn test_async_fetch() {
let result = fetch_data().await.unwrap();
assert!(!result.is_empty());
}
#[tokio::test]
async fn test_timeout() {
let result = tokio::time::timeout(
Duration::from_secs(1),
slow_operation(),
).await;
assert!(result.is_err());
}
6. Mockall 模拟外部依赖
6.1 生成 Mock
use mockall::automock;
#[automock]
pub trait Database {
async fn get_user(&self, id: u64) -> Result<User, DbError>;
async fn save_user(&self, user: &User) -> Result<(), DbError>;
}
6.2 使用 Mock
#[tokio::test]
async fn test_user_service() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_get_user()
.with(eq(42))
.times(1)
.returning(|_| Ok(User { id: 42, name: "Alice".to_string() }));
let service = UserService::new(mock_db);
let user = service.find_user(42).await.unwrap();
assert_eq!(user.name, "Alice");
}
6.3 序列与状态
mock_db
.expect_save_user()
.times(2)
.returning(|_| Ok(()));
// 验证调用顺序
let mut seq = Sequence::new();
mock_db.expect_get_user()
.times(1)
.in_sequence(&mut seq)
.returning(|_| Ok(User::default()));
mock_db.expect_save_user()
.times(1)
.in_sequence(&mut seq)
.returning(|_| Ok(()));
7. Property-based Testing
使用 proptest 自动生成随机输入,发现边界情况。
use proptest::prelude::*;
proptest! {
#[test]
fn test_reverse_reverse(xs: Vec<i32>) {
let mut rev = xs.clone();
rev.reverse();
rev.reverse();
prop_assert_eq!(xs, rev);
}
#[test]
fn test_add_commutative(a: i32, b: i32) {
prop_assert_eq!(a + b, b + a);
}
#[test]
fn test_sort_idempotent(mut xs: Vec<i32>) {
xs.sort();
let sorted_once = xs.clone();
xs.sort();
prop_assert_eq!(sorted_once, xs);
}
}
失败时自动最小化反例:
测试失败,最小化后的反例:xs = [3, 1, 2]
8. 代码覆盖率
8.1 tarpaulin
cargo install cargo-tarpaulin
# HTML 报告
cargo tarpaulin --out Html
# 忽略测试代码
# 在 Cargo.toml 中添加
# [package.metadata.tarpaulin]
# exclude-files = ["tests/*"]
8.2 llvm-cov
cargo install cargo-llvm-cov
# 生成并查看覆盖率
cargo llvm-cov --html
open target/llvm-cov/html/index.html
# CI 集成:lcov 格式
cargo llvm-cov --lcov --output-path lcov.info
8.3 CI 集成示例
# .github/workflows/test.yml
- name: Run tests with coverage
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: lcov.info
| 工具 | 用途 |
|---|---|
| thiserror | 结构化错误类型定义 |
| anyhow | 应用级错误快速传播 |
| mockall | 模拟 trait/外部依赖 |
| proptest | 属性/随机测试 |
| cargo-tarpaulin | 代码覆盖率分析 |
| cargo-llvm-cov | 基于 LLVM 的覆盖率 |
Rust 的测试生态提供了从单元测试到覆盖率的全链路支持,配合编译期类型检查,可以在部署前捕获绝大多数 bug。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「rust」更多文章
Rust 系统编程与性能优化:零拷贝、内存剖析与编译调优
Rust 系统编程深度实践:操作系统原语、零拷贝 I/O、mmap 内存映射、性能剖析(cargo flamegraph)、编译器优化(LTO/PGO)、Benchmark 与内存分析,以及 no_std 嵌入式场景。
Rust 桌面端与 WASM:Tauri、WebAssembly 与嵌入式开发
Rust 跨平台开发全景:Tauri 替代 Electron 的轻量级架构、WASM 编译(wasm-bindgen/wasm-pack)、WASI 运行时、嵌入式 Rust(no_std/embedded-hal),以及跨平台发布策略。
Rust 核心概念详解:所有权、Trait 与宏系统
Rust 核心概念全景解析:所有权模型、借用检查器、生命周期、Trait 系统、泛型编程、声明宏与过程宏,以及 Cargo 工作区与 crates 生态体系。