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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 05:45:57