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

ASP.NET Core缓存层XUnit单元测试相关问题咨询

ASP.NET缓存类XUnit单元测试解决方案

一、正确模拟IHttpClientFactory(无需修改方法参数)

你不需要把HttpClient作为GetCacheGetOrCreateAsync的参数,因为你的类已经通过IHttpClientFactory注入HttpClient,只要正确Mock这个工厂就能让模拟的响应生效。核心思路是给Mock的工厂返回一个预设响应的HttpClient,具体步骤如下:

  1. 用HttpMessageHandler模拟外部接口的响应
  2. 基于这个Handler创建HttpClient
  3. 让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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 11:57:12