如何在Rust测试套件结束后清理Postgres Testcontainer容器?
问题描述
我正在为Rust测试实现Postgres Testcontainer的复用逻辑,定义了如下结构体:
use sea_orm::{ConnectOptions, DatabaseConnection, SqlxPostgresConnector}; use std::process::Command; use testcontainers::runners::AsyncRunner; use testcontainers::ContainerAsync; use testcontainers_modules::postgres::Postgres; use tokio::runtime::Runtime; use uuid::Uuid; const USER_NAME: &'static str = "<USERNAME>"; const PASSWORD: &'static str = "<PASSWORD>"; pub struct PostgresTestContainer { ctn: ContainerAsync<Postgres>, } impl PostgresTestContainer { pub async fn create() -> anyhow::Result<Self> { // Create the postgres container and return let ctn = Postgres::default() .with_user(USER_NAME) .with_password(PASSWORD) .start() .await?; Ok(Self { ctn }) } pub async fn new_db(&self) -> anyhow::Result<DatabaseConnection> { // Expose and provide a port to localhost let host_port = self.ctn.get_host_port_ipv4(5432).await?; let db_name = Uuid::new_v4().to_string(); // Create the database url using the host port let db_url = format!("postgres://{USER_NAME}:{PASSWORD}@localhost:{host_port}/{db_name}"); let db = SqlxPostgresConnector::connect(ConnectOptions::new(&db_url)).await?; Ok(db) } }
当前通过lazy_static创建静态容器供所有测试复用:
lazy_static! { static ref CTN: AsyncOnce<PostgresTestContainer> = AsyncOnce::new(async { PostgresTestContainer::create().await.expect("Could not create postgres container") }); ... } #[tokio::test] async fn login_with_correct_pwd_returns_tokens() -> Result<(), anyhow::Error> { let db = CTN.get().await.new_db().await?; ... }
实现运行正常,但静态对象不会调用Drop方法,无法在测试套件结束后自动停止容器,请问该如何实现容器的清理?
解决方案
方法1:结合once_cell与进程退出钩子
用once_cell替代lazy_static管理容器实例,同时通过std::atexit注册异步清理逻辑,确保进程退出前停止容器:
- 先给
PostgresTestContainer添加停止容器的方法:
impl PostgresTestContainer { // ... 原有方法 ... pub async fn stop(&self) -> anyhow::Result<()> { self.ctn.stop().await?; Ok(()) } }
- 重构容器初始化逻辑:
use once_cell::sync::OnceCell; use std::sync::Arc; use tokio::runtime::Runtime; // 用OnceCell存储容器的Arc实例 static POSTGRES_CTN: OnceCell<Arc<PostgresTestContainer>> = OnceCell::new(); async fn init_container() -> Arc<PostgresTestContainer> { let ctn = PostgresTestContainer::create().await.expect("Failed to start Postgres container"); let arc = Arc::new(ctn); // 注册进程退出钩子,在进程结束时停止容器 let arc_clone = arc.clone(); std::atexit(move || { // 创建Runtime阻塞执行异步停止逻辑 let rt = Runtime::new().expect("Failed to create Tokio runtime"); if let Err(e) = rt.block_on(arc_clone.stop()) { eprintln!("Warning: Failed to stop Postgres container: {}", e); } }); arc } // 测试中获取容器的统一入口 pub async fn get_test_container() -> Arc<PostgresTestContainer> { POSTGRES_CTN.get_or_init(|| { let rt = Runtime::new().expect("Failed to create Tokio runtime"); rt.block_on(init_container()) }).clone() }
- 测试中使用:
#[tokio::test] async fn login_with_correct_pwd_returns_tokens() -> Result<(), anyhow::Error> { let ctn = get_test_container().await; let db = ctn.new_db().await?; // ... 测试逻辑 ... Ok(()) }
方法2:使用测试框架的全局Fixture(以rstest为例)
利用rstest的#[once] fixture实现全局容器的初始化与自动清理,fixture会在所有测试执行前初始化一次,测试全部结束后触发清理:
- 添加
rstest依赖到Cargo.toml:
[dev-dependencies] rstest = "0.18"
- 定义全局Fixture:
use rstest::rstest; use std::sync::Arc; use tokio::runtime::Runtime; // 全局一次性Fixture,所有测试共享 #[fixture] #[once] async fn shared_postgres_container() -> Arc<PostgresTestContainer> { let ctn = PostgresTestContainer::create().await.expect("Failed to start Postgres container"); let arc = Arc::new(ctn); // 用Guard结构体在最后一个引用释放时触发清理 struct ContainerCleanupGuard(Arc<PostgresTestContainer>); impl Drop for ContainerCleanupGuard { fn drop(&mut self) { let rt = Runtime::new().expect("Failed to create Tokio runtime"); if let Err(e) = rt.block_on(self.0.stop()) { eprintln!("Warning: Failed to stop Postgres container: {}", e); } } } // 生成Guard,确保测试结束后执行清理 tokio::spawn(async move { ContainerCleanupGuard(arc.clone()) }); arc }
- 测试中注入Fixture:
#[rstest] async fn login_with_correct_pwd_returns_tokens(shared_postgres_container: Arc<PostgresTestContainer>) -> Result<(), anyhow::Error> { let db = shared_postgres_container.new_db().await?; // ... 测试逻辑 ... Ok(()) }
方法3:自定义测试入口(集成测试场景)
如果是集成测试,可以自定义测试入口函数,手动控制容器的生命周期:
#[tokio::main] async fn main() -> anyhow::Result<()> { // 初始化容器 let ctn = PostgresTestContainer::create().await?; // 运行所有测试(需要自行处理测试发现,或使用`test` crate的API) let test_result = run_all_tests().await; // 测试结束后停止容器 ctn.stop().await?; test_result } // 模拟测试运行逻辑,实际可使用`test` crate的测试发现功能 async fn run_all_tests() -> anyhow::Result<()> { login_with_correct_pwd_returns_tokens().await?; // ... 其他测试 ... Ok(()) } #[tokio::test] async fn login_with_correct_pwd_returns_tokens() -> Result<(), anyhow::Error> { // 这里可以直接使用全局容器,或者从main传递上下文 // ... 测试逻辑 ... Ok(()) }
内容的提问来源于stack exchange,提问作者Christian Schmid
相关产品推荐
相关产品推荐

