使用 Docker 进行 Rust 集成测试简介
\n集成测试是使用 Rust 开发可靠服务器应用程序的关键阶段。与单元测试不同,集成测试检查组件与真实依赖项(如数据库、缓存、外部 API)的交互。使用 Docker 可以隔离测试环境并确保可重复性。在本文中,我们将分析高级技术:并行启动容器、状态管理、模拟服务以及通过 Tokio 处理异步操作。
\n\n测试环境配置:Docker 和 testcontainers
\n对于 Rust 中的集成测试,使用 testcontainers crate 很方便。它允许以编程方式启动 Docker 容器,等待它们准备就绪,并正确清理资源。我们从基本配置开始。
安装依赖项
\n添加到 Cargo.toml:
[dev-dependencies]\ntestcontainers = "0.21"\ntokio = { version = "1", features = ["full"] }\nreqwest = { version = "0.12", features = ["json"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n基本示例:PostgreSQL 容器
\n让我们创建一个测试,启动 PostgreSQL,执行迁移并检查数据插入:
\nuse testcontainers::{clients, images::postgres::Postgres, Container};\nuse tokio_postgres::{NoTls, Client};\n\nasync fn setup_db() -> (Client, Container<'static, Postgres>) {\n let docker = clients::Cli::default();\n let pg_image = Postgres::default();\n let container = docker.run(pg_image);\n let host = container.get_host();\n let port = container.get_host_port_ipv4(5432);\n let connection_string = format!(\n "host={host} port={port} user=postgres dbname=postgres",\n );\n let (client, connection) = tokio_postgres::connect(&connection_string, NoTls)\n .await\n .expect("Failed to connect");\n tokio::spawn(async move {\n if let Err(e) = connection.await {\n eprintln!("Connection error: {e}");\n }\n });\n (client, container)\n}\n\n#[tokio::test]\nasync fn test_insert_user() {\n let (client, _container) = setup_db().await;\n client\n .simple_query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)")\n .await\n .unwrap();\n client\n .execute("INSERT INTO users (name) VALUES ($1)", &[&"Alice"])\n .await\n .unwrap();\n let rows = client.query("SELECT name FROM users", &[]).await.unwrap();\n assert_eq!(rows.len(), 1);\n assert_eq!(rows[0].get::<_, String>(0), "Alice");\n}\n由于 Container 的析构函数,容器在测试完成后会自动删除。这省去了手动清理的麻烦。
并行运行测试和资源管理
\n默认情况下,Rust 在并行线程中执行测试。如果每个测试都启动自己的容器,可能会导致端口冲突和 Docker 过载。让我们考虑两种方法。
\n通过动态分配使用不同端口
\ntestcontainers crate 会自动在主机上分配空闲端口。这解决了冲突问题,但增加了资源消耗。对于重量级容器(例如数据库),最好使用共享实例:
use once_cell::sync::Lazy;\nuse testcontainers::clients;\n\nstatic DOCKER: Lazy<clients::Cli> = Lazy::new(|| clients::Cli::default());\n\nasync fn get_shared_postgres() -> (tokio_postgres::Client, Container<'static, Postgres>) {\n let pg_image = Postgres::default();\n let container = DOCKER.run(pg_image);\n // ... 连接方式同前一个示例\n (client, container)\n}\n然而,这种方法有风险:测试可能会通过共享状态相互影响。建议在单个容器内使用单独的数据库,或者重置状态