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

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());
}

关键修改点

  1. 移除了不必要的FinbuckleController和IMediator模拟,简化测试逻辑;
  2. 为ActionExecutedContext设置了Result属性,值为包含业务数据的ObjectResult;
  3. 验证CacheResponseAsync时精准匹配传入的tenantResponse对象,提升测试严谨性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 01:25:54