【问题标题】:XUnit how to mock IMemoryCache ASP.NET CoreXUnit 如何模拟 IMemoryCache ASP.NET Core
【发布时间】:2019-04-12 21:08:39
【问题描述】:

我了解 IMemoryCache.Set 是一种扩展方法,因此无法模拟。人们已经为这种情况提供了解决方法,例如 NKosi here 提供的一种解决方法。我想知道如何为我的数据访问层实现这一点,其中我的 MemoryCache 返回一个值,当找不到它从数据库获取数据时,将其设置为 MemoryCache 并返回所需的值。

    public string GetMessage(int code)
    {
        if(myMemoryCache.Get("Key") != null)
        {
            var messages= myMemoryCache.Get<IEnumerable<MyModel>>("Key");
            return messages.Where(x => x.Code == code).FirstOrDefault().Message;
        }

        using (var connection = dbFactory.CreateConnection())
        {
            var cacheOptions = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(1) };
            const string sql = @"SELECT Code, Message FROM MyTable";

            var keyPairValueData = connection.Query<KeyPairValueData>(sql);

            myMemoryCache.Set("Key", keyPairValueData, cacheOptions );

            return keyPairValueData.Where(x => x.Code == code).FirstOrDefault().Message;
        }
    }

以下是我的单元测试 - 当然它不起作用,因为我无法模拟 IMemoryCache

    [Fact]
    public void GetMessage_ReturnsString()
    {
        //Arrange
        // Inserting some data here to the InMemoryDB

        var memoryCacheMock = new Mock<IMemoryCache>();

        //Act
        var result = new DataService(dbConnectionFactoryMock.Object, memoryCacheMock.Object).GetMessage(1000);

        //assert xunit
        Assert.Equal("Some message", result);
    }

【问题讨论】:

  • 由于使用了所有扩展,因此尝试完全模拟该接口很棘手。这就是为什么我开始建议使用链接答案中的实际内存缓存。
  • 当前测试有什么问题
  • 对象引用未设置为 myMemoryCache.Set("Key", keyPairValueData, cacheOptions ); 处的对象错误实例;
  • 您是否尝试过使用链接答案中的方法?
  • 创建一个新的 MemoryCache 实例而不是模拟工作非常好,我已经完成了,但不确定是否应该使用实际对象而不是模拟。因此,@Kenneth 的第一个建议非常有效。不过还没有尝试过他的嘲讽建议。

标签: c# unit-testing mocking moq xunit


【解决方案1】:

我要说的第一件事是为什么不使用真正的内存缓存?它会更好地验证行为,无需模拟它:

// Arrange
var memCache = new MemoryCache("name", new NameValueCollection());

//Act
var result = new DataService(dbConnectionFactoryMock.Object, memCache).GetMessage(1000);

// Assert: has been added to cache
memCache.TryGetValue("Key", out var result2);
Assert.Equal("Some message", result2);

// Assert: value is returned
Assert.Equal("Some message", result);

如果你真的想模拟它,这里有一个关于如何做到这一点的指南:

因为它是一个扩展方法,所以你需要确保它可以按原样调用。在您的情况下发生的是扩展方法将调用模拟。由于您没有提供预期的行为,它可能会失败。

您需要查看扩展方法的代码,检查它访问的内容,然后确保您的模拟符合预期的行为。该代码可在此处获得: https://github.com/aspnet/Caching/blob/master/src/Microsoft.Extensions.Caching.Abstractions/MemoryCacheExtensions.cs#L77

这是代码:

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
    {
        using (var entry = cache.CreateEntry(key))
        {
            if (options != null)
            {
                entry.SetOptions(options);
            }

            entry.Value = value;
        }

        return value;
    }

因此,您可以看到它访问了CreateEnty,并期望从中获取一个对象。然后它调用SetOptions 并在条目上分配Value

你可以这样模拟它:

var entryMock = new Mock<ICacheEntry>();
memoryCacheMock.Setup(m => m.CreateEntry(It.IsAny<object>())
               .Returns(entryMock.Object);

// maybe not needed
entryMock.Setup(e => e.SetOptions(It.IsAny<MemoryCacheEntryOptions>())
         ...

当你这样做时,扩展方法将在模拟上被调用,它会返回模拟的条目。您可以修改实现并使其为所欲为。

【讨论】:

  • 我相信你的意思是说为什么要使用真正的内存缓存而不是“为什么不”?如果我错了,请纠正我!
  • 不,我的意思是“为什么不使用真正的内存缓存”
  • @Kenneth 我相信第一个 sn-p 示例已关闭,因为 MemoryCache 没有无参数构造函数。
  • 啊,是的,你是对的。您需要将其更改为.Returns(entryMock.Object),因为您想要返回实例,而不是模拟容器。我已经更新了答案
  • 谢谢@Kenneth。在您的第一个 sn-p 中,您已按照 Nkosi 的建议传递了 2 个参数。就我而言,它只允许我传递 MemoryCacheOptions。 var cache = new MemoryCache(new MemoryCacheOptions());它适用于 options 参数,但只是想知道我在这里做了什么不同。除了这个小小的改变之外,这两种方法对我来说都很好。
猜你喜欢
  • 1970-01-01
  • 2020-01-07
  • 2017-04-02
  • 2016-05-12
  • 1970-01-01
  • 2019-10-23
  • 2022-01-13
  • 1970-01-01
  • 2016-11-14
相关资源
最近更新 更多