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

集成测试中IDistributedMemoryCache值无法持久化的问题排查

问题根源分析

你当前的核心问题是缓存初始化的服务实例不匹配:在ConfigureServices中调用BuildServiceProvider()会创建一个临时服务容器,你在此容器中初始化的内存缓存数据,和测试时应用实际使用的服务容器是完全独立的两个实例——所以调试日志能读到值,但测试用的是全新的缓存实例,自然返回null。

另外,直接使用主Redis实例会污染数据,下面提供两种可行解决方案:


方案一:修复内存缓存初始化逻辑

方法1:通过HostedService延迟初始化

创建测试专用的HostedService,在应用启动时初始化缓存(和测试用的服务容器共用实例):

public class TestCacheInitializer : IHostedService
{
    private readonly IDistributedCache _cache;

    public TestCacheInitializer(IDistributedCache cache)
    {
        _cache = cache;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        TestUtils.SeedCacheForTests(_cache);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

修改CustomWebApplicationFactory的配置逻辑:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.ConfigureServices(services =>
    {
        // 移除原有SQL和Redis服务的逻辑保持不变
        var sqlDescriptor = services.Single(
            d => d.ServiceType == typeof(DbContextOptions<ApiContext>));
        var redisDescriptor = services.Single(
            d => d.ServiceType == typeof(IDistributedCache) 
                && d.ImplementationType?.Name == "RedisCache");

        services.Remove(sqlDescriptor);
        services.Remove(redisDescriptor);
        
        // 注册内存数据库
        services.AddDbContext<ApiContext>(options =>
        {
            options.UseInMemoryDatabase("InMemoryDbForTesting");
        });

        // 注册内存缓存
        services.AddDistributedMemoryCache();

        // 注册缓存初始化服务(确保和测试用容器共用实例)
        services.AddHostedService<TestCacheInitializer>();

        // 初始化数据库(原有逻辑保留)
        var sp = services.BuildServiceProvider();
        using var scope = sp.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApiContext>();
        TestUtils.SeedDbForTests(db);
    });
}

方法2:在测试类中直接初始化缓存

在测试类中通过Fixture获取应用的服务上下文,直接操作测试用的缓存实例:

public class SignupTest : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public SignupTest(CustomWebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();

        // 获取测试用缓存实例并初始化
        using var scope = factory.Services.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();
        TestUtils.SeedCacheForTests(cache);
    }

    [Fact]
    public async Task Signup_WithValidSession_ReturnsSuccess()
    {
        // 测试逻辑编写
    }
}

方案二:使用独立Redis测试实例(推荐,更贴近生产环境)

利用Docker动态创建专属Redis容器,测试结束自动销毁,完全隔离主实例:

步骤1:安装Docker.DotNet包

在测试项目中安装Docker.DotNet NuGet包,用于操作Docker容器。

步骤2:修改CustomWebApplicationFactory

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>, IAsyncLifetime 
    where TStartup : class
{
    private DockerClient _dockerClient;
    private string _redisContainerId;
    private string _redisConnectionString;

    public async Task InitializeAsync()
    {
        // 初始化Docker客户端(Windows下使用npipe协议)
        _dockerClient = new DockerClientConfiguration(
            new Uri("npipe://./pipe/docker_engine")).CreateClient();

        // 拉取Redis镜像(本地无镜像时自动下载)
        await _dockerClient.Images.CreateImageAsync(
            new ImagesCreateParameters { FromImage = "redis:latest" },
            null,
            new Progress<JSONMessage>());

        // 创建并启动Redis容器
        var containerResponse = await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters
        {
            Image = "redis:latest",
            Name = $"test-redis-{Guid.NewGuid()}",
            ExposedPorts = new Dictionary<string, EmptyStruct> { { "6379/tcp", new EmptyStruct() } },
            HostConfig = new HostConfig
            {
                PortBindings = new Dictionary<string, IList<PortBinding>>
                {
                    { "6379/tcp", new List<PortBinding> { new PortBinding { HostPort = "0" } } }
                }
            }
        });

        _redisContainerId = containerResponse.ID;
        await _dockerClient.Containers.StartContainerAsync(_redisContainerId, new ContainerStartParameters());

        // 获取容器映射端口,构造连接字符串
        var containerInspect = await _dockerClient.Containers.InspectContainerAsync(_redisContainerId);
        var mappedPort = containerInspect.NetworkSettings.Ports["6379/tcp"].First().HostPort;
        _redisConnectionString = $"localhost:{mappedPort}";
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 移除原有SQL服务并替换为内存数据库
            var sqlDescriptor = services.Single(
                d => d.ServiceType == typeof(DbContextOptions<ApiContext>));
            services.Remove(sqlDescriptor);
            services.AddDbContext<ApiContext>(options =>
            {
                options.UseInMemoryDatabase("InMemoryDbForTesting");
            });

            // 移除原有Redis服务,替换为测试实例
            var redisDescriptor = services.Single(
                d => d.ServiceType == typeof(IDistributedCache) 
                    && d.ImplementationType?.Name == "RedisCache");
            services.Remove(redisDescriptor);

            services.AddStackExchangeRedisCache(options =>
            {
                options.Configuration = _redisConnectionString;
            });

            // 初始化数据库和缓存
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ApiContext>();
            var cache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();
            
            TestUtils.SeedDbForTests(db);
            TestUtils.SeedCacheForTests(cache);
        });
    }

    public async Task DisposeAsync()
    {
        // 测试结束后停止并删除Redis容器
        if (!string.IsNullOrEmpty(_redisContainerId))
        {
            await _dockerClient.Containers.StopContainerAsync(_redisContainerId, new ContainerStopParameters());
            await _dockerClient.Containers.RemoveContainerAsync(_redisContainerId, new ContainerRemoveParameters { Force = true });
        }
        _dockerClient.Dispose();
    }
}

此方案每次测试都会启动全新的Redis容器,测试完成自动销毁,既不会污染主实例,又能模拟生产环境的Redis行为。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 21:46:06