ASP.NET Core缓存层XUnit单元测试相关问题咨询
ASP.NET缓存类XUnit单元测试解决方案
一、正确模拟IHttpClientFactory(无需修改方法参数)
你不需要把HttpClient作为GetCacheGetOrCreateAsync的参数,因为你的类已经通过IHttpClientFactory注入HttpClient,只要正确Mock这个工厂就能让模拟的响应生效。核心思路是给Mock的工厂返回一个预设响应的HttpClient,具体步骤如下:
- 用
HttpMessageHandler模拟外部接口的响应 - 基于这个Handler创建HttpClient
- 让Mock的
IHttpClientFactory返回这个HttpClient
代码示例:
// 准备测试数据 var testData = Encoding.UTF8.GetBytes("mock response data"); // 模拟HttpMessageHandler,预设响应 var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>() ) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new ByteArrayContent(testData) }); // 创建HttpClient并绑定模拟Handler var httpClient = new HttpClient(mockHandler.Object); // Mock IHttpClientFactory,返回上面的HttpClient var mockHttpClientFactory = new Mock<IHttpClientFactory>(); mockHttpClientFactory.Setup(f => f.CreateClient(It.IsAny<string>())) .Returns(httpClient);
把这个Mock的IHttpClientFactory注入你的Cache类,调用GetCacheGetOrCreateAsync时,私有方法GetDataByteArrayAsync就会用到这个预设响应的HttpClient。
二、IMemoryCache的测试方案
传闻“无法用Moq直接Mock缓存”不准确,两种方案可选:
方案1:使用实际的MemoryCache实例(推荐)
IMemoryCache是轻量级服务,测试时直接创建真实实例完全可行,比Mock更直观,能准确验证缓存的读写行为:
// 创建真实的MemoryCache实例 var memoryCache = new MemoryCache(new MemoryCacheOptions()); // 注入到你的Cache类 var cacheService = new Cache(mockHttpClientFactory.Object, memoryCache);
方案2:用Moq模拟IMemoryCache
如果一定要Mock,需要处理缓存的GetOrCreateAsync逻辑,示例如下:
var mockMemoryCache = new Mock<IMemoryCache>(); object cacheValue = null; // 模拟TryGetValue和Set操作 mockMemoryCache.Setup(m => m.TryGetValue(It.IsAny<object>(), out cacheValue)) .Returns(false); // 第一次调用缓存未命中 mockMemoryCache.Setup(m => m.CreateEntry(It.IsAny<object>())) .Returns(Mock.Of<ICacheEntry>(entry => entry.Value == cacheValue && entry.ExpirationTokens == null && entry.PostEvictionCallbacks == null));
但这种方式需要处理更多细节,不如用真实实例简单。
三、缓存行为单元测试通用实践
1. 测试缓存命中逻辑
验证第一次调用触发外部请求,第二次直接返回缓存数据:
[Fact] public async Task GetCacheGetOrCreateAsync_WhenCalledTwice_UsesCacheSecondTime() { // 准备Mock和缓存实例(用真实MemoryCache) var testData = Encoding.UTF8.GetBytes("test"); var mockHandler = new Mock<HttpMessageHandler>(); var callCount = 0; mockHandler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new ByteArrayContent(testData) }) .Callback(() => callCount++); // 统计调用次数 var httpClient = new HttpClient(mockHandler.Object); var mockFactory = new Mock<IHttpClientFactory>(); mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient); var memoryCache = new MemoryCache(new MemoryCacheOptions()); var cacheService = new Cache(mockFactory.Object, memoryCache); // 第一次调用 var result1 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "test-key" }); // 第二次调用 var result2 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "test-key" }); // 验证两次结果一致,且外部请求只触发一次 Assert.Equal(testData, result1); Assert.Equal(testData, result2); Assert.Equal(1, callCount); }
2. 测试缓存失效逻辑
设置缓存过期时间,验证过期后重新触发外部请求:
[Fact] public async Task GetCacheGetOrCreateAsync_AfterExpiration_RefetchesData() { var testData1 = Encoding.UTF8.GetBytes("version1"); var testData2 = Encoding.UTF8.GetBytes("version2"); var callCount = 0; var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(() => { callCount++; return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new ByteArrayContent(callCount == 1 ? testData1 : testData2) }; }); var httpClient = new HttpClient(mockHandler.Object); var mockFactory = new Mock<IHttpClientFactory>(); mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient); var memoryCache = new MemoryCache(new MemoryCacheOptions()); var cacheService = new Cache(mockFactory.Object, memoryCache); // 第一次调用,缓存数据1 var result1 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "expire-key", CacheExpirationMinutes = 0.01 // 设置10秒过期 }); // 等待缓存过期 await Task.Delay(TimeSpan.FromSeconds(11)); // 第二次调用,缓存失效,获取数据2 var result2 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "expire-key", CacheExpirationMinutes = 0.01 }); Assert.Equal(testData1, result1); Assert.Equal(testData2, result2); Assert.Equal(2, callCount); }
3. 测试缓存键唯一性
验证不同CacheDataSetting生成不同缓存键,避免数据混淆:
[Fact] public async Task GetCacheGetOrCreateAsync_DifferentCacheKeys_StoreSeparateData() { var data1 = Encoding.UTF8.GetBytes("data1"); var data2 = Encoding.UTF8.GetBytes("data2"); var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => { return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new ByteArrayContent(req.RequestUri.ToString().Contains("key1") ? data1 : data2) }; }); var httpClient = new HttpClient(mockHandler.Object); var mockFactory = new Mock<IHttpClientFactory>(); mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient); var memoryCache = new MemoryCache(new MemoryCacheOptions()); var cacheService = new Cache(mockFactory.Object, memoryCache); var result1 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "key1" }); var result2 = await cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "key2" }); Assert.Equal(data1, result1); Assert.Equal(data2, result2); }
4. 测试异常处理
验证外部接口报错时,缓存服务的行为(比如不缓存错误结果):
[Fact] public async Task GetCacheGetOrCreateAsync_WhenExternalRequestFails_ThrowsException() { var mockHandler = new Mock<HttpMessageHandler>(); mockHandler.Protected() .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError }); var httpClient = new HttpClient(mockHandler.Object); var mockFactory = new Mock<IHttpClientFactory>(); mockFactory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient); var memoryCache = new MemoryCache(new MemoryCacheOptions()); var cacheService = new Cache(mockFactory.Object, memoryCache); // 验证抛出异常 await Assert.ThrowsAsync<HttpRequestException>(() => cacheService.GetCacheGetOrCreateAsync(new CacheDataSetting { Url = "http://test.com", CacheKey = "error-key" }) ); // 验证缓存中没有错误数据 var exists = memoryCache.TryGetValue("error-key", out _); Assert.False(exists); }
内容的提问来源于stack exchange,提问作者fanfan17
相关产品推荐
相关产品推荐

