集成测试中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
相关产品推荐
相关产品推荐

