You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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注册异步清理逻辑,确保进程退出前停止容器:

  1. 先给PostgresTestContainer添加停止容器的方法:
impl PostgresTestContainer {
    // ... 原有方法 ...
    
    pub async fn stop(&self) -> anyhow::Result<()> {
        self.ctn.stop().await?;
        Ok(())
    }
}
  1. 重构容器初始化逻辑:
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()
}
  1. 测试中使用:
#[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会在所有测试执行前初始化一次,测试全部结束后触发清理:

  1. 添加rstest依赖到Cargo.toml:
[dev-dependencies]
rstest = "0.18"
  1. 定义全局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
}
  1. 测试中注入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:34:56