Testcontainers集成测试问题:容器启动后退出及超时错误求助
问题描述
原本基于inMemoryDatabase & XUnit的集成测试,计划用testcontainers 3.5重构为Docker镜像运行,但遇到两个问题:
System.TimeoutException: The operation has timed out,出现在XUnit的CreateClassFixtureAsync(Type fixtureType)方法中;Docker API responded with status code=Conflict, response={message:Container %some_id% is not running},原因是Docker容器启动后立即变为Exited状态。
相关代码如下:
Factory.cs
public class IntegrationTestApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime { private const string ImageName = "ubuntu:16.04"; // tips #1 private const string Database = "master"; private const string Username = "sa"; private const string Password = "myStr0ng!&Passw0rd!"; private int MsSqlPort = Random.Shared.Next(10000, 60000); private readonly IContainer _mssqlContainer; public IntegrationTestMalumApplicationFactory() { _mssqlContainer = new ContainerBuilder() .WithImage(ImageName) .WithName(Guid.NewGuid().ToString()) .WithEnvironment("ACCEPT_EULA", "Y") .WithEnvironment("SQLCMDUSER", Username) .WithEnvironment("SQLCMDPASSWORD", Password) .WithEnvironment("MSSQL_SA_PASSWORD", Password) .WithEnvironment("MSSQL_PID", "Evaluation") .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MsSqlPort)) .WithPortBinding(MsSqlPort, true) .WithExposedPort(MsSqlPort) .WithCleanUp(true) .Build(); } protected override void ConfigureWebHost(IWebHostBuilder builder) { var host = _mssqlContainer.Hostname; var port = _mssqlContainer.GetMappedPublicPort(MsSqlPort); builder.ConfigureTestServices(services => { services.RemoveAll(typeof(DbContextOptions<DataAccess.Context>)); services.AddDbContextPool<DataAccess.Context>((options, optionsBuilder) => { optionsBuilder.UseSqlServer( $"Server={host},{port};Database={Database};User Id={Username};Password={Password};TrustServerCertificate=true"); }); }); } public async Task InitializeAsync() => await _mssqlContainer.StartAsync(); public async new Task DisposeAsync() => await _mssqlContainer.StopAsync(); }
Test.cs
public class IntegrationTestMalumApi : IClassFixture<IntegrationTestApplicationFactory> { protected readonly HttpClient TestClient; protected readonly ApplicationUrl ApplicationUrl; public IntegrationTestMalumApi(IntegrationTestApplicationFactory applicationFactory) { TestClient = applicationFactory.CreateClient(); ApplicationUrl = new ApplicationUrl("http://localhost:1324"); } [Fact] public async Task GetCats_WithThreeCats_ReturnHttpCodeOK() { // to do something } }
补充说明:
- 尝试过
mcr.microsoft.com/mssql/server:2022-preview-ubuntu-22.04镜像,但只有ubuntu:16.04能"运行"; ApplicationUrl是存储API路由的类;- 问题有时出现在
XunitTestClassRunner.cs的CreateClassFixtureAsync,有时出现在InitializeAsync()调用_mssqlContainer.StartAsync()时; - 即使基础配置也无法正常运行。
问题根源与修正方案
核心错误:镜像选择完全错误
ubuntu:16.04是纯基础Linux镜像,没有预装MSSQL服务,容器启动后没有长期运行的进程,立刻就会退出(Exited状态),这是所有问题的根源。你之前尝试的MSSQL官方镜像才是正确选择,之前无法运行大概率是配置问题(比如密码策略、环境变量错误)。
其他关键错误点
- 端口配置错误:MSSQL容器默认监听内部1433端口,你自定义的随机
MsSqlPort在容器内没有对应的服务,等待策略永远无法检测到端口可用,最终触发超时。 - 配置时机错误:
ConfigureWebHost在容器启动前就会执行,此时调用GetMappedPublicPort会获取到无效的端口值,导致数据库连接字符串错误。 - DisposeAsync实现错误:用
new修饰DisposeAsync会隐藏基类方法,可能导致容器无法正确停止和清理。
修正后的代码
Factory.cs
using Testcontainers.MsSql; public class IntegrationTestApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime { private const string Database = "master"; private const string Username = "sa"; private const string Password = "MyStr0ng!&Passw0rd123"; // 密码需符合MSSQL复杂度要求:至少8位,包含大小写、数字、特殊字符 private readonly MsSqlContainer _mssqlContainer; public IntegrationTestApplicationFactory() { // 使用官方MSSQL容器镜像,Testcontainers提供的MsSqlContainer封装了所有默认配置 _mssqlContainer = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-latest") .WithPassword(Password) .WithCleanUp(true) .Build(); } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { services.RemoveAll(typeof(DbContextOptions<DataAccess.Context>)); // 直接使用容器提供的连接字符串,无需手动拼接 services.AddDbContextPool<DataAccess.Context>((options, optionsBuilder) => { optionsBuilder.UseSqlServer( _mssqlContainer.GetConnectionString(), opt => opt.EnableRetryOnFailure()); }); }); } public async Task InitializeAsync() { await _mssqlContainer.StartAsync(); // 可选:如果需要初始化数据库结构,可在这里执行迁移脚本 // await InitializeDatabase(); } public async Task DisposeAsync() { await _mssqlContainer.StopAsync(); await _mssqlContainer.DisposeAsync(); await base.DisposeAsync(); } // 可选:初始化数据库结构 // private async Task InitializeDatabase() // { // await using var scope = Services.CreateScope(); // var dbContext = scope.ServiceProvider.GetRequiredService<DataAccess.Context>(); // await dbContext.Database.MigrateAsync(); // } }
Test.cs(小调整)
public class IntegrationTestMalumApi : IClassFixture<IntegrationTestApplicationFactory> { protected readonly HttpClient TestClient; protected readonly ApplicationUrl ApplicationUrl; public IntegrationTestMalumApi(IntegrationTestApplicationFactory applicationFactory) { TestClient = applicationFactory.CreateClient(); // 使用TestClient的BaseAddress,无需硬编码端口 ApplicationUrl = new ApplicationUrl(TestClient.BaseAddress.ToString()); } [Fact] public async Task GetCats_WithThreeCats_ReturnHttpCodeOK() { // 测试逻辑示例 var response = await TestClient.GetAsync(ApplicationUrl.CatsEndpoint); response.EnsureSuccessStatusCode(); Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); } }
额外说明
- MSSQL密码要求:官方镜像要求SA密码必须符合复杂度规则(至少8字符,包含大小写字母、数字、特殊字符),之前你用的密码
myStr0ng!&Passw0rd!虽然看起来复杂,但全小写开头可能不符合要求,修正后改成首字母大写并添加数字; - Testcontainers封装类:使用
MsSqlContainer而不是通用ContainerBuilder,可以省去手动配置环境变量、端口、等待策略等繁琐步骤,Testcontainers会自动处理; - 连接字符串获取:
GetConnectionString()会自动生成正确的连接字符串,包含映射后的主机端口、认证信息等,避免手动拼接出错。
内容的提问来源于stack exchange,提问作者tender
相关产品推荐
相关产品推荐

