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

WebApplicationFactory集成测试:Redis配置覆盖顺序异常排查

问题描述

我正在为一个通过Redis通信的Web API编写集成测试,尝试用容器化Redis替代原有Redis服务器执行测试。问题在于,WebApplicationFactory会先使用项目的appsettings.Development.json配置和旧的IConnectionMultiplexer实例启动API,而旧配置中的主机已离线,导致连接失败。我需要让API使用连接容器化Redis的新IConnectionMultiplexer,但当前执行顺序错误:先启动旧配置的API再替换服务,连接异常阻碍了测试进行。此外,我还有其他基于配置注入的服务,因此必须先覆盖配置,再启动API代码。


API代码

public static class RedisConnectionConfiguration
{
    public static void AddRedisConnection(this IServiceCollection serviceCollection, IConfiguration config)
    {
        var redisConfig = config.GetSection("Redis").Get<RedisConfiguration>();

        serviceCollection.AddHostedService<RedisSubscription>();

        serviceCollection.AddSingleton(redisConfig);
        var redis = ConnectionMultiplexer.Connect(redisConfig.Host); // 这里失败,因为还没覆盖Redis:Host配置
        serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
    }
}

集成测试代码

OrderManagerApiFactory

public class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
    private const string Password = "Test1234!";

    private readonly TestcontainersContainer _redisContainer;
    private readonly int _externalPort = Random.Shared.Next(10_000, 60_000);

    public OrderManagerApiFactory()
    {
        _redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("redis:alpine")
            .WithEnvironment("REDIS_PASSWORD", Password)
            .WithPortBinding(_externalPort, 6379)
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
            .Build();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Development");

        builder.ConfigureLogging(logging =>
        {
            logging.ClearProviders();
        });

        builder.ConfigureAppConfiguration(config =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                { "Redis:Host", $"localhost:{_externalPort},password={Password},allowAdmin=true" },
                { "Redis:Channels:Main", "main:new:order" },
            });
        });

        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll(typeof(IConnectionMultiplexer));
            services.AddSingleton<IConnectionMultiplexer>(_ =>
                ConnectionMultiplexer.Connect($"localhost:{_externalPort},password={Password},allowAdmin=true"));
        });
    }

    public async Task InitializeAsync()
    {
        await _redisContainer.StartAsync();
    }

    public new async Task DisposeAsync()
    {
        await _redisContainer.DisposeAsync();
    }
}

OrderManagerTests

public class OrderManagerTests : IClassFixture<OrderManagerApiFactory>, IAsyncLifetime
{
    private readonly OrderManagerApiFactory _apiFactory;

    public OrderManagerTests(OrderManagerApiFactory apiFactory)
    {
        _apiFactory = apiFactory;
    }

    [Fact]
    public async Task Test()
    {
        // Arrange
        var configuration = _apiFactory.Services.GetRequiredService<IConfiguration>();
        var redis = _apiFactory.Services.GetRequiredService<IConnectionMultiplexer>();
        var channel = configuration.GetValue<string>("Redis:Channels:Main");

        // Act
        await redis.GetSubscriber().PublishAsync(channel, "ping");

        // Assert
    }

    public Task InitializeAsync()
    {
        return Task.CompletedTask;
    }

    public Task DisposeAsync()
    {
        return Task.CompletedTask;
    }
}

解决方案

核心问题是API在服务注册阶段就直接创建了Redis连接,此时测试的配置覆盖还未生效,导致使用旧配置连接失败。以下是具体修复步骤:

1. 修改API的Redis注册逻辑,延迟连接创建

将直接创建ConnectionMultiplexer改为工厂模式注入,让连接逻辑延迟到第一次使用实例时执行,确保此时测试配置已经覆盖完成。

修改后的RedisConnectionConfiguration:

public static class RedisConnectionConfiguration
{
    public static void AddRedisConnection(this IServiceCollection serviceCollection, IConfiguration config)
    {
        var redisConfig = config.GetSection("Redis").Get<RedisConfiguration>();

        serviceCollection.AddHostedService<RedisSubscription>();

        serviceCollection.AddSingleton(redisConfig);
        // 改用工厂模式,延迟创建连接,直到第一次获取IConnectionMultiplexer时执行
        serviceCollection.AddSingleton<IConnectionMultiplexer>(sp => 
        {
            var redisSettings = sp.GetRequiredService<RedisConfiguration>();
            return ConnectionMultiplexer.Connect(redisSettings.Host);
        });
    }
}

2. 简化测试工厂配置,利用覆盖后的配置自动创建连接

现在配置覆盖会先于连接创建生效,无需手动移除和重新注册IConnectionMultiplexer,测试工厂可简化为:

public class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
    private const string Password = "Test1234!";

    private readonly TestcontainersContainer _redisContainer;

    public OrderManagerApiFactory()
    {
        _redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("redis:alpine")
            .WithEnvironment("REDIS_PASSWORD", Password)
            .WithPortBinding(6379, true) // 自动分配外部端口,避免冲突
            .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
            .Build();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Development");

        builder.ConfigureLogging(logging =>
        {
            logging.ClearProviders();
        });

        builder.ConfigureAppConfiguration(configBuilder =>
        {
            // 获取容器自动分配的映射端口
            var redisPort = _redisContainer.GetMappedPort(6379);
            configBuilder.AddInMemoryCollection(new Dictionary<string, string>
            {
                { "Redis:Host", $"localhost:{redisPort},password={Password},allowAdmin=true" },
                { "Redis:Channels:Main", "main:new:order" },
            });
        });
    }

    public async Task InitializeAsync()
    {
        await _redisContainer.StartAsync();
    }

    public new async Task DisposeAsync()
    {
        await _redisContainer.DisposeAsync();
    }
}

3. 优化说明

  • 延迟连接创建:避免在服务注册阶段(配置未覆盖时)执行Redis连接,确保使用测试环境的容器化Redis配置。
  • 自动端口分配:Testcontainers自动分配外部端口,避免手动指定端口导致的冲突问题,提升测试稳定性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 18:43:12