ASP.NET Core中CachedAttribute单元测试缓存写入验证失败求助
缓存特性单元测试写入失败问题排查
我为控制器实现了一个CachedAttribute特性,该特性会优先从缓存读取数据,缓存未命中时执行控制器逻辑并将结果写入缓存。
特性实现代码
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] internal sealed class CachedAttribute : Attribute, IAsyncActionFilter { private readonly int _timeToLiveMinutes; public CachedAttribute(int timeToLiveMinutes) { _timeToLiveMinutes = timeToLiveMinutes; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>(); var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey); if (!string.IsNullOrEmpty(cachedResponse)) { var contentResult = new ContentResult { Content = cachedResponse, ContentType = "application/json", StatusCode = 200 }; context.Result = contentResult; return; } var executedContext = await next(); // 执行控制器逻辑 if (executedContext.Result is ObjectResult objectResult) { await cacheService.CacheResponseAsync(cacheKey, objectResult.Value, _timeToLiveMinutes); } } private static string GenerateCacheKeyFromRequest(HttpRequest request) { var keyBuilder = new StringBuilder(); keyBuilder.Append($"{request.Path}"); foreach (var (key, value) in request.Query.OrderBy(x => x.Key)) { keyBuilder.Append($"|{key}-{value}"); } return keyBuilder.ToString(); } }
验证缓存读取的单元测试(执行成功)
public async Task Cashed_attribute_gets_data_from_cache() { var mockCacheService = new Mock<IResponseCacheService>(); mockCacheService.Setup(x => x.GetCachedResponseAsync(It.IsAny<string>())) .ReturnsAsync(() => JsonConvert.SerializeObject(GetTenantResponse())); mockCacheService.Setup(x => x.CacheResponseAsync(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<int>())) .Returns(() => Task.CompletedTask); await using var provider = new ServiceCollection() .AddSingleton<IResponseCacheService>(_ => mockCacheService.Object) .BuildServiceProvider(true); var httpContext = new DefaultHttpContext { Request = { Path = "/somepath/identifier" }, RequestServices = provider }; var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), new ModelStateDictionary()); var actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller: null); ActionExecutionDelegate mockDelegate = () => Task.FromResult(new ActionExecutedContext(actionContext, new List<IFilterMetadata>(), null)); var sut = new CachedAttribute(1); await sut.OnActionExecutionAsync(actionExecutingContext, mockDelegate); mockCacheService.Verify(x => x.GetCachedResponseAsync(It.IsAny<string>()), Times.Once()); mockCacheService.Verify(x => x.CacheResponseAsync(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<int>()), Times.Never()); }
验证缓存写入的单元测试(执行失败)
[Fact] public async Task Cashed_attribute_adds_data_to_cache() { var mockCacheService = new Mock<IResponseCacheService>(); mockCacheService.Setup(x => x.GetCachedResponseAsync(It.IsAny<string>())) .ReturnsAsync(() => string.Empty); mockCacheService.Setup(x => x.CacheResponseAsync(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<int>())) .Returns(() => Task.CompletedTask); await using var provider = new ServiceCollection() .AddSingleton<IResponseCacheService>(_ => mockCacheService.Object) .BuildServiceProvider(true); var httpContext = new DefaultHttpContext { Request = { Path = "/somepath/identifier" }, RequestServices = provider }; var mockMediator = new Mock<IMediator>(); mockMediator.Setup(x => x.Send(It.IsAny<GetTenantByIdentifierQuery>(), default)) .ReturnsAsync(() => GetTenantResponse()); var controller = new FinbuckleController(mockMediator.Object) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { RequestServices = provider } } }; var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), new ModelStateDictionary()); var actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller); ActionExecutionDelegate mockDelegate = () => Task.FromResult(new ActionExecutedContext(actionContext, new List<IFilterMetadata>(), controller)); var sut = new CachedAttribute(1); await sut.OnActionExecutionAsync(actionExecutingContext, mockDelegate); mockCacheService.Verify(x => x.GetCachedResponseAsync(It.IsAny<string>()), Times.Once()); mockCacheService.Verify(x => x.CacheResponseAsync(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<int>()), Times.Once()); }
排查思路与解决方案
问题根源
测试失败的核心原因是:模拟的ActionExecutionDelegate返回的ActionExecutedContext没有设置Result属性,导致特性中的if (executedContext.Result is ObjectResult objectResult)条件不成立,直接跳过了缓存写入逻辑。
另外,测试中实例化真实控制器和模拟IMediator属于冗余操作——单元测试CachedAttribute本身时,不需要依赖控制器的实际执行逻辑,只需确保next()返回的上下文包含符合要求的ObjectResult即可。
修复后的测试代码
修改mockDelegate部分,手动构造带有ObjectResult的ActionExecutedContext:
[Fact] public async Task Cashed_attribute_adds_data_to_cache() { var mockCacheService = new Mock<IResponseCacheService>(); mockCacheService.Setup(x => x.GetCachedResponseAsync(It.IsAny<string>())) .ReturnsAsync(() => string.Empty); mockCacheService.Setup(x => x.CacheResponseAsync(It.IsAny<string>(), It.IsAny<object>(), It.IsAny<int>())) .Returns(() => Task.CompletedTask); await using var provider = new ServiceCollection() .AddSingleton<IResponseCacheService>(_ => mockCacheService.Object) .BuildServiceProvider(true); var httpContext = new DefaultHttpContext { Request = { Path = "/somepath/identifier" }, RequestServices = provider }; var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), new ModelStateDictionary()); var actionExecutingContext = new ActionExecutingContext(actionContext, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller: null); // 构造带有ObjectResult的执行上下文 var tenantResponse = GetTenantResponse(); var objectResult = new ObjectResult(tenantResponse) { StatusCode = 200 }; ActionExecutionDelegate mockDelegate = () => Task.FromResult(new ActionExecutedContext(actionContext, new List<IFilterMetadata>(), null) { Result = objectResult }); var sut = new CachedAttribute(1); await sut.OnActionExecutionAsync(actionExecutingContext, mockDelegate); mockCacheService.Verify(x => x.GetCachedResponseAsync(It.IsAny<string>()), Times.Once()); mockCacheService.Verify(x => x.CacheResponseAsync(It.IsAny<string>(), It.Is<object>(tenantResponse), It.IsAny<int>()), Times.Once()); }
关键修改点
- 移除了不必要的
FinbuckleController和IMediator模拟,简化测试逻辑; - 为
ActionExecutedContext设置了Result属性,值为包含业务数据的ObjectResult; - 验证
CacheResponseAsync时精准匹配传入的tenantResponse对象,提升测试严谨性。
内容的提问来源于stack exchange,提问作者user351479
相关产品推荐
相关产品推荐

