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
相关产品推荐
相关产品推荐

