测试策略实战
问题
Rust 项目应该如何设计测试策略?
答案
测试金字塔
单元测试
// 在同一文件中,使用 #[cfg(test)] 模块
pub fn fibonacci(n: u32) -> u32 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fibonacci() {
assert_eq!(fibonacci(0), 0);
assert_eq!(fibonacci(1), 1);
assert_eq!(fibonacci(10), 55);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_overflow() {
// 测试应该 panic 的情况
}
}
使用 Trait 进行 Mock
// 定义 Trait 便于测试
#[async_trait::async_trait]
pub trait UserRepository: Send + Sync {
async fn find_by_id(&self, id: i64) -> Result<User, Error>;
}
// 生产实现
struct PgUserRepository { pool: PgPool }
#[async_trait::async_trait]
impl UserRepository for PgUserRepository {
async fn find_by_id(&self, id: i64) -> Result<User, Error> {
sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(&self.pool)
.await
}
}
// 测试用 Mock
#[cfg(test)]
struct MockUserRepo {
users: HashMap<i64, User>,
}
#[cfg(test)]
#[async_trait::async_trait]
impl UserRepository for MockUserRepo {
async fn find_by_id(&self, id: i64) -> Result<User, Error> {
self.users.get(&id).cloned()
.ok_or(Error::NotFound)
}
}
集成测试
// tests/api_test.rs(tests/ 目录下的文件自动成为集成测试)
use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
#[tokio::test]
async fn test_create_user() {
let app = create_app(); // 创建完整应用
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/users")
.header("content-type", "application/json")
.body(Body::from(r#"{"name": "Alice"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
测试工具对比
| 工具 | 用途 |
|---|---|
#[test] | 内置单元测试 |
#[tokio::test] | 异步测试 |
mockall | 自动生成 mock |
wiremock | HTTP mock 服务器 |
testcontainers | Docker 容器化测试 |
cargo-nextest | 更快的测试运行器 |
常见面试问题
Q1: Rust 测试中如何处理数据库依赖?
答案:
- Trait 抽象 + Mock:单元测试用 Mock 实现
- testcontainers:集成测试启动真实 Docker 数据库
- SQLite 内存:用 SQLite
:memory:替代 Postgres(简单场景) - 事务回滚:每个测试在事务中执行,测试后回滚