配置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
相关产品推荐
相关产品推荐

