跳到主要内容

测试策略实战

问题

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
wiremockHTTP mock 服务器
testcontainersDocker 容器化测试
cargo-nextest更快的测试运行器

常见面试问题

Q1: Rust 测试中如何处理数据库依赖?

答案

  1. Trait 抽象 + Mock:单元测试用 Mock 实现
  2. testcontainers:集成测试启动真实 Docker 数据库
  3. SQLite 内存:用 SQLite :memory: 替代 Postgres(简单场景)
  4. 事务回滚:每个测试在事务中执行,测试后回滚

相关链接