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

C#单元测试:MemoryCache与SemaphoreSlim并发请求验证

使用真实MemoryCache做单元测试的合理性及优化方案

一、当前方案的合理性分析

使用真实的MemoryCache实例做单元测试是合理的,但存在优缺点:

优点

  • 测试逻辑贴近真实运行环境,能准确验证缓存的实际行为(比如缓存命中、过期机制),避免模拟缓存时出现与真实实现不一致的偏差。
  • 实现简单,无需额外模拟TryGetValue、Set等缓存方法,减少测试代码的复杂度。

缺点

  • 若测试间未做好隔离,缓存状态会污染后续测试(比如前一个测试的缓存数据影响下一个测试结果)。
  • 无法精准控制缓存异常场景(比如模拟缓存写入失败、强制缓存失效),限制了测试场景的覆盖范围。

二、更优实现方案

根据测试目标的不同,可选择以下两种优化方向:

方案1:模拟IMemoryCache,精准控制缓存行为

如果测试核心是验证并发场景下仅调用一次API的逻辑,模拟IMemoryCache更灵活,能完全控制缓存状态,同时精准统计API调用次数。

修改测试代码如下:

public async Task GetSigningKeysFromJwkAsync_ConcurrentRequests_OnlyOneFetchAndOthersUseCache()
{
    // Arrange
    // 模拟IMemoryCache:第一次TryGetValue返回空,后续返回缓存数据
    object cachedKeys = null;
    var mockCache = new Mock<IMemoryCache>();
    mockCache.Setup(m => m.TryGetValue(It.IsAny<object>(), out cachedKeys))
             .Returns(() => cachedKeys != null);
    mockCache.Setup(m => m.Set(It.IsAny<object>(), It.IsAny<object>(), It.IsAny<MemoryCacheEntryOptions>()))
             .Callback((object key, object value, MemoryCacheEntryOptions _) => cachedKeys = value);

    // 模拟HttpClient,统计API调用次数
    var mockHttpHandler = new Mock<HttpMessageHandler>();
    mockHttpHandler.Protected()
                   .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                   .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
                   {
                       Content = new StringContent("{\"issuer1\":\"{\\\"keys\\\":[{\\\"kty\\\":\\\"RSA\\\",\\\"e\\\":\\\"AQAB\\\",\\\"n\\\":\\\"test\\\"}]}\"}")
                   });
    var httpClient = new HttpClient(mockHttpHandler.Object);

    var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    mockHttpClientFactory.Setup(f => f.CreateClient("client")).Returns(httpClient);

    var publicKeyService = new PublicKeyService(mockHttpClientFactory.Object, mockCache.Object);
    var bffUrl = new Uri("https://dummy-link/jwks");

    // Act
    var tasks = Enumerable.Range(0, 10)
                          .Select(_ => publicKeyService.GetSigningKeysFromJwkAsync(bffUrl))
                          .ToList();
    await Task.WhenAll(tasks);

    // Assert
    // 核心验证:API仅被调用一次
    mockHttpHandler.Protected()
                   .Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
    // 验证所有请求返回相同结果
    var firstResult = tasks[0].Result;
    foreach (var task in tasks)
    {
        Assert.Equal(firstResult, task.Result);
    }
}

方案2:保留真实MemoryCache,强化测试隔离

如果需要验证缓存的完整行为(比如过期、缓存命中逻辑),可以继续使用真实MemoryCache,但要确保测试隔离:

  • 每个测试都创建全新的MemoryCache实例(你当前代码已做到这一点)。
  • 补充API调用次数的验证,这是该测试场景的核心目标。

修改后的测试关键部分:

// Arrange
var mockHttpHandler = new Mock<HttpMessageHandler>();
mockHttpHandler.Protected()
               .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
               .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
               {
                   Content = new StringContent("{\"issuer1\":\"{\\\"keys\\\":[{\\\"kty\\\":\\\"RSA\\\",\\\"e\\\":\\\"AQAB\\\",\\\"n\\\":\\\"test\\\"}]}\"}")
               });
var httpClient = new HttpClient(mockHttpHandler.Object);

// ... 其他初始化代码

// Assert
// 验证API仅被调用一次
mockHttpHandler.Protected()
               .Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
// 验证所有请求返回相同结果
var firstResult = tasks[0].Result;
foreach (var task in tasks)
{
    Assert.Equal(firstResult, task.Result);
}

三、总结

  • 使用真实MemoryCache是合理的,适合需要验证缓存实际行为的场景,实现成本低。
  • 若核心测试目标是并发下的API调用控制,模拟IMemoryCache更灵活,能精准控制缓存状态并验证依赖调用次数。
  • 无论选择哪种方案,都必须补充API调用次数的验证,这才是该测试场景的核心验收标准。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 06:45:14