使用 Docker 进行 Rust 集成测试:从理论到实践
现代 Rust 开发不仅需要检查单个函数(单元测试),还需要测试组件与外部服务(数据库、缓存、API)的交互。使用 Docker 进行集成测试可以隔离环境、保证可重复性,并避免"在我机器上能运行"的问题。本文将分析如何为 Rust 应用设置使用 Docker 的集成测试。
为什么选择 Docker 进行 Rust 集成测试?
Docker 解决了三个关键问题:
- 环境隔离 — 测试在干净的容器中运行,不受本地设置影响
- 可重复性 — 在所有开发机器和 CI/CD 中条件一致
- 依赖管理 — 自动启动 PostgreSQL、Redis、Kafka 及其他服务
项目设置:结构与依赖
让我们创建一个集成测试项目,针对与 PostgreSQL 配合使用的 Actix-web 网络服务器。
# Cargo.toml[package]name = "rust-docker-testing"version = "0.1.0"edition = "2021"
[dependencies]actix-web = "4"serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }dotenv = "0.15"
[dev-dependencies]testcontainers = "0.14"reqwest = { version = "0.11", features = ["json"] }注意 testcontainers — 这是一个 Rust 库,用于直接从测试中管理 Docker 容器。
项目结构
推荐用于集成测试的结构:
.├── src/│ ├── main.rs│ ├── lib.rs│ ├── models.rs│ └── handlers.rs├── tests/│ ├── common/│ │ └── mod.rs # 测试的通用工具│ ├── integration_tests.rs # 主要集成测试│ └── docker-compose.yml # 用于手动启动├── Dockerfile└── .env步骤 1:使用 Docker 编写集成测试
让我们创建一个测试,在 Docker 中启动 PostgreSQL,运行迁移,并检查 API。
// tests/integration_tests.rsuse testcontainers::clients;use testcontainers::images::postgres::Postgres;use sqlx::postgres::PgPoolOptions;use std::net::TcpListener;
async fn setup_test_db() -> (String, testcontainers::Container) { let docker = clients::Cli::default(); let postgres_image = Postgres::default(); let container = docker.run(postgres_image); let host = container.get_host().unwrap(); let port = container.get_host_port_ipv4(5432).unwrap(); let database_url = format!("postgres://postgres:postgres@{}:{}/postgres", host, port); // 执行迁移 let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("无法连接到 Postgres"); sqlx::query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)") .execute(&pool) .await .expect("迁移失败"); (database_url, container)}
#[tokio::test]async fn test_create_user() { let (db_url, _container) = setup_test_db().await; // 在随机端口启动应用 let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let server = actix_web::HttpServer::new(move || { actix_web::App::new() .app_data(actix_web::web::Data::new(db_url.clone())) .route("/users", actix_web::web::post().to(handlers::create_user)) }) .listen(listener) .unwrap() .run(); tokio::spawn(server); // 等待服务器启动 tokio::ti