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

配置TestContainers实现集成测试中每个测试对应独立数据库容器

如何为每个测试用例创建独立的TestContainers PostgreSQL容器

问题描述

我正在为Campaign仓库搭建集成测试,使用TestContainers NuGet包时遇到问题:期望为每个测试用例创建独立的数据库容器,但实际运行时容器仅创建一次就被销毁,无法为每个测试生成新容器。请问如何配置TestContainers实现该行为,或需检查哪些配置点?

现有代码

PostgresFixture.cs

namespace MyProject.Tests.IntegrationTests
{
    public sealed class PostgresFixture : IAsyncLifetime
    {
        public readonly PostgreSqlContainer PostgreSqlContainer = new PostgreSqlBuilder().Build();
        public Task InitializeAsync()
        {
            return PostgreSqlContainer.StartAsync();
        }

        public Task DisposeAsync()
        {
            return PostgreSqlContainer.DisposeAsync().AsTask();
        }

        public async Task ExecuteCmdAsync(string query)
        {
            using (DbConnection connection = new NpgsqlConnection(PostgreSqlContainer.GetConnectionString()))
            {
                using (DbCommand command = new NpgsqlCommand())
                {
                    connection.Open();
                    command.Connection = connection;
                    command.CommandText = query;

                    await command.ExecuteNonQueryAsync();
                }
            }
        }
    }
}

CampaignRepositoryTests.cs

namespace MyProject.Tests.IntegrationTests
{
    public class CampaignRepositoryTests : IClassFixture<PostgresFixture>
    {
        private readonly PostgresFixture _fixture;
        private readonly ICampaignsRepository _campaignsRepository;
        private readonly DbContextOptions<CampaignsDatabaseContext> options;
        private readonly CampaignsDatabaseContext _context;
       
        public CampaignRepositoryTests(
            PostgresFixture fixture)
        {
            _fixture = fixture;

            TestDomainEventBus DomainEventBus = new();
            TestIntegrationEventStoreDispatcher IntegrationEventStoreDispatcher = new();

            options = new DbContextOptionsBuilder<CampaignsDatabaseContext>()
                .UseNpgsql(_fixture.PostgreSqlContainer.GetConnectionString())
                .Options;

            _context = new CampaignsDatabaseContext(options, DomainEventBus, IntegrationEventStoreDispatcher);

            _context.Database.Migrate();

            _campaignsRepository = new CampaignRepository(_context);

            SeedDatabase(@"TestData/campaigns_insert.sql").Wait();
        }

        private async Task SeedDatabase(string filename)
        {
            var sql = File.ReadAllText(filename);
            await _fixture.ExecuteCmdAsync(sql);
        }

        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign()
        {
            var campaignId = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624775"));
        }
        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign2()
        {
            var campaignId = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624776"));
        }
        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign3()
        {
            var campaignId = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624777"));
        }
        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign4()
        {
            var campaignId = await _campaignsRepository.FindById(Guid.Parse("61a83cfe-0e5c-49fc-988a-5b8fe6614673"));
        }
    }
    class TestDomainEventBus : IDomainEventBus
    {
        public Task Publish<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : IDomainEvent
        {
            return Task.CompletedTask;
        }
    }
    class TestIntegrationEventStoreDispatcher : IIntegrationEventStoreDispatcher
    {
        public ValueTask Dispatch(CancellationToken cancellationToken = default)
        {
            return ValueTask.CompletedTask;
        }
    }
}

问题根源

你当前使用IClassFixture<PostgresFixture>,这是xUnit的类级别Fixture,意味着整个测试类只会创建一次Fixture实例,PostgreSQL容器也只会启动一次,所有测试用例共享同一个容器和数据库,这就是容器只创建一次的核心原因。

解决方案

方案一:移除IClassFixture,在测试类中独立管理容器生命周期

修改测试类,去掉IClassFixture<PostgresFixture>,改为实现IAsyncLifetime,让每个测试方法拥有独立的容器实例:

namespace MyProject.Tests.IntegrationTests
{
    public class CampaignRepositoryTests : IAsyncLifetime
    {
        private readonly PostgresFixture _fixture;
        private readonly ICampaignsRepository _campaignsRepository;
        private readonly CampaignsDatabaseContext _context;
       
        public CampaignRepositoryTests()
        {
            _fixture = new PostgresFixture();
        }

        public async Task InitializeAsync()
        {
            // 启动容器
            await _fixture.InitializeAsync();
            
            var domainEventBus = new TestDomainEventBus();
            var integrationEventStoreDispatcher = new TestIntegrationEventStoreDispatcher();

            var options = new DbContextOptionsBuilder<CampaignsDatabaseContext>()
                .UseNpgsql(_fixture.PostgreSqlContainer.GetConnectionString())
                .Options;

            _context = new CampaignsDatabaseContext(options, domainEventBus, integrationEventStoreDispatcher);

            // 执行数据库迁移
            await _context.Database.MigrateAsync();

            _campaignsRepository = new CampaignRepository(_context);

            // 初始化测试数据
            await SeedDatabase(@"TestData/campaigns_insert.sql");
        }

        public async Task DisposeAsync()
        {
            // 销毁容器和DbContext
            await _fixture.DisposeAsync();
            await _context.DisposeAsync();
        }

        private async Task SeedDatabase(string filename)
        {
            var sql = File.ReadAllText(filename);
            await _fixture.ExecuteCmdAsync(sql);
        }

        // 测试方法保持不变
        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign()
        {
            var campaign = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624775"));
            // 添加你的断言逻辑
        }

        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign2()
        {
            var campaign = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624776"));
        }

        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign3()
        {
            var campaign = await _campaignsRepository.FindById(Guid.Parse("47a3ff0b-98f9-45a8-a6e5-c6821d624777"));
        }

        [Fact]
        public async Task Find_By_Id_Should_Return_Campaign4()
        {
            var campaign = await _campaignsRepository.FindById(Guid.Parse("61a83cfe-0e5c-49fc-988a-5b8fe6614673"));
        }
    }

    // TestDomainEventBus和TestIntegrationEventStoreDispatcher代码不变
}

方案二:优化Fixture类,确保每次实例化都创建新容器

如果想保留Fixture的封装性,可以修改PostgresFixture,将容器的创建移到InitializeAsync方法中,确保每个Fixture实例对应一个新容器:

namespace MyProject.Tests.IntegrationTests
{
    public sealed class PostgresFixture : IAsyncLifetime
    {
        public PostgreSqlContainer PostgreSqlContainer { get; private set; }

        public Task InitializeAsync()
        {
            // 每次初始化都创建新容器
            PostgreSqlContainer = new PostgreSqlBuilder().Build();
            return PostgreSqlContainer.StartAsync();
        }

        public Task DisposeAsync()
        {
            return PostgreSqlContainer.DisposeAsync().AsTask();
        }

        public async Task ExecuteCmdAsync(string query)
        {
            using (DbConnection connection = new NpgsqlConnection(PostgreSqlContainer.GetConnectionString()))
            {
                using (DbCommand command = new NpgsqlCommand())
                {
                    connection.Open();
                    command.Connection = connection;
                    command.CommandText = query;

                    await command.ExecuteNonQueryAsync();
                }
            }
        }
    }
}

之后按照方案一的方式修改测试类,去掉IClassFixture,在测试类中管理Fixture的生命周期即可。

关键检查点

  • Fixture作用域:IClassFixture是类级别的共享实例,要实现每个测试独立容器必须放弃它,改用每个测试实例独立创建Fixture。
  • 异步操作处理:避免在构造函数中用.Wait()阻塞异步操作,所有异步初始化逻辑要放到IAsyncLifetime.InitializeAsync中执行,确保资源正确初始化。
  • 容器唯一性:TestContainers默认会为每个容器生成唯一名称,不要手动指定固定名称,否则会出现容器冲突。
  • 资源清理:确保每个容器在测试结束后通过DisposeAsync正确销毁,避免残留容器占用资源。

内容的提问来源于stack exchange,提问作者Striker

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 16:29:51