如何在WebApplicationFactory中覆盖数据库连接字符串?
问题
我需要为API编写集成测试,现有Program.cs及自定义Startup.cs(通过WebApplicationBuilder的扩展方法模拟.NET 6之前的行为):
// Program.cs WebApplication.CreateBuilder(args) .ConfigureLogging() .ConfigureServices() .Build() .Initialize() .ConfigurePipeline() .Run(); // Startup.cs public static class Startup { public static WebApplicationBuilder ConfigureLogging(this WebApplicationBuilder builder) { // Configure logging return builder; } public static WebApplicationBuilder ConfigureServices(this WebApplicationBuilder builder) { var services = builder.Services; var configuration = builder.Configuration; services.AddPooledDbContextFactory<AppDbContext>((serviceProvider, options) => { var connectionString = configuration.GetConnectionString("Database"); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); dataSourceBuilder.EnableDynamicJson(); options.UseNpgsql(dataSourceBuilder.Build()); }); return builder; } public static WebApplication ConfigurePipeline(this WebApplication app) { // Register middleware app.UseRouting(); app.MapControllers(); return app; } public static WebApplication Initialize(this WebApplication app) { // Resolve DbContext and call .Migrate() return app; } }
我尝试创建自定义WebApplicationFactory<>,使用Testcontainers.PostgreSql指定测试数据库,工厂代码如下:
public class MyFactory : WebApplicationFactory<IAssemblyMarker>, IAsyncLifetime { private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder() .WithUsername("username") .WithPassword("password") .WithDatabase("mydb") .Build(); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureLogging(logging => { logging.ClearProviders(); }); // If I use ConfigureTestServices, the test first calls ConfigureTestServices, // then ConfigureServices, and then Startup.ConfigureServices (the extension method) //builder.ConfigureTestServices(services => builder.ConfigureServices(services => { var connectionString = _postgreSqlContainer.GetConnectionString(); services.RemoveAll<AppDbContext>(); services.RemoveAll<IDbContextFactory<AppDbContext>>(); services.RemoveAll<IDataTypeBuilder>(); services.AddSingleton<IDataTypeBuilder, NpgsqlDataTypeBuilder>(); services.AddPooledDbContextFactory<AppDbContext>(options => { var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString) { Name = "IntegrationTests App" }; options.UseNpgsql(dataSourceBuilder.Build()); }); }); } public async Task InitializeAsync() { await _postgreSqlContainer.StartAsync(); } async Task IAsyncLifetime.DisposeAsync() { await _postgreSqlContainer.StopAsync(); } }
调用_myFactory.CreateClient()时总是抛出异常,因为appsettings.json中的连接串指向本地PostgreSQL容器(测试时该容器未启动)。通过断点发现,测试调用CreateClient后,会先执行WebApplicationFactory的ConfigureServices(或ConfigureTestServices),再执行Startup.ConfigureServices,导致测试容器的连接串未生效,测试仍尝试连接本地数据库。
请问正确的修复方案是什么?自定义Startup类(扩展方法形式的ConfigureServices)是否是问题根源?
修复方案
核心问题
问题不在于扩展方法形式的Startup,而是配置加载顺序和服务注册覆盖逻辑:
Startup.ConfigureServices从builder.Configuration读取连接字符串,测试中仅替换服务注册但未修改配置中的连接串,导致Startup仍使用原配置。- 若测试的
ConfigureServices在Startup.ConfigureServices之前执行,Startup的服务注册会覆盖测试中的注册,最终还是使用本地数据库连接串。
正确解决步骤
方案1:覆盖配置源(推荐)
直接修改测试中的配置系统,让Startup.ConfigureServices读取到测试容器的连接串,无需手动替换服务:
protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureLogging(logging => { logging.ClearProviders(); }); // 关键:添加内存配置源,优先级高于appsettings.json builder.ConfigureAppConfiguration((context, config) => { var connectionString = _postgreSqlContainer.GetConnectionString(); config.AddInMemoryCollection(new Dictionary<string, string> { { "ConnectionStrings:Database", connectionString } }); }); // 如需添加测试专属服务,用ConfigureTestServices(在Startup.ConfigureServices之后执行) builder.ConfigureTestServices(services => { // 这里可添加测试依赖替换逻辑 }); }
方案2:用ConfigureTestServices覆盖服务注册
若必须手动替换IDbContextFactory,改用ConfigureTestServices——它会在Startup.ConfigureServices之后执行,确保测试的服务注册覆盖原注册:
protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureLogging(logging => { logging.ClearProviders(); }); builder.ConfigureTestServices(services => { var connectionString = _postgreSqlContainer.GetConnectionString(); services.RemoveAll<IDbContextFactory<AppDbContext>>(); services.AddPooledDbContextFactory<AppDbContext>(options => { var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString) { Name = "IntegrationTests App" }; options.UseNpgsql(dataSourceBuilder.Build()); }); }); }
注意事项
- 确保
IAsyncLifetime.InitializeAsync正确启动测试容器,WebApplicationFactory.CreateClient会在容器启动后执行,此时连接串有效。 - 优先选择方案1,无需修改业务代码,测试逻辑更贴合真实运行环境。
内容的提问来源于stack exchange,提问作者TheDoomDestroyer

