使用Moq模拟IMemoryCache并抛出异常

47

我试图使用Moq来模拟IMemoryCache。但我遇到了这个错误:

  

System.NotSupportedException的异常发生在Moq.dll中,但未在用户代码中处理。

  

额外信息:表达式引用了不属于模拟对象的方法:x => x.Get<String>(It.IsAny<String>())。

我的模拟代码:

namespace Iag.Services.SupplierApiTests.Mocks
{
    public static class MockMemoryCacheService
    {
        public static IMemoryCache GetMemoryCache()
        {
            Mock<IMemoryCache> mockMemoryCache = new Mock<IMemoryCache>();
            mockMemoryCache.Setup(x => x.Get<string>(It.IsAny<string>())).Returns("");<---------- **ERROR**
            return mockMemoryCache.Object;
        }
    }
}

为什么会出现这个错误?

以下是被测试的代码:

var cachedResponse = _memoryCache.Get<String>(url);

其中_memoryCache的类型是IMemoryCache

如何模拟上述的_memoryCache.Get<String>(url)并使其返回null?

编辑: 如果要对_memoryCache.Set<String>(url, response)进行相同的操作,我该怎么做呢?我不在意它返回什么,我只需要将该方法添加到模拟中,以便在调用时不会抛出异常。

根据这个问题的答案,我尝试了:

mockMemoryCache
    .Setup(m => m.CreateEntry(It.IsAny<object>())).Returns(null as ICacheEntry);

因为在 memoryCache 扩展中,它显示使用了 CreateEntry 来设置 Set。但是它出现了 "对象引用未设置为对象实例" 的错误。

4个回答

85
根据 MemoryCacheExtensions.cs 的源代码, Get<TItem> 扩展方法使用以下内容。
public static object Get(this IMemoryCache cache, object key)
{
    cache.TryGetValue(key, out object value);
    return value;
}

public static TItem Get<TItem>(this IMemoryCache cache, object key)
{
    return (TItem)(cache.Get(key) ?? default(TItem));
}

public static bool TryGetValue<TItem>(this IMemoryCache cache, object key, out TItem value)
{
    if (cache.TryGetValue(key, out object result))
    {
        if (result is TItem item)
        {
            value = item;
            return true;
        }
    }

    value = default;
    return false;
}

请注意,本质上它是使用 TryGetValue(Object, out Object) 方法。

考虑到使用 Moq 无法模拟扩展方法,请尝试模拟扩展方法访问的接口成员。

参考 Moq 的快速入门 更新 MockMemoryCacheService,以便为测试正确设置 TryGetValue 方法。

public static class MockMemoryCacheService {
    public static IMemoryCache GetMemoryCache(object expectedValue) {
        var mockMemoryCache = new Mock<IMemoryCache>();
        mockMemoryCache
            .Setup(x => x.TryGetValue(It.IsAny<object>(), out expectedValue))
            .Returns(true);
        return mockMemoryCache.Object;
    }
}

来自评论

Note that when mocking TryGetValue (in lieu of Get), the out parameter must be declared as an object even if it isn't.

For example:

int expectedNumber = 1; 
object expectedValue = expectedNumber. 

If you don't do this then it will match a templated extension method of the same name.

这里是一个使用修改后的服务来模拟 memoryCache.Get<String>(url) 并使其返回 null 的示例

[TestMethod]
public void _IMemoryCacheTestWithMoq() {
    var url = "fakeURL";
    object expected = null;

    var memoryCache = MockMemoryCacheService.GetMemoryCache(expected);

    var cachedResponse = memoryCache.Get<string>(url);

    Assert.IsNull(cachedResponse);
    Assert.AreEqual(expected, cachedResponse);
}

更新

同样的过程也可以应用于Set<>扩展方法,它看起来像这样。

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value) {
    var entry = cache.CreateEntry(key);
    entry.Value = value;
    entry.Dispose();

    return value;
}

这种方法利用了CreateEntry方法,该方法返回一个ICacheEntry,也会受到影响。因此,设置模拟以返回一个模拟的条目,就像以下示例中一样。

[TestMethod]
public void _IMemoryCache_Set_With_Moq() {
    var url = "fakeURL";
    var response = "json string";

    var memoryCache = Mock.Of<IMemoryCache>();
    var cachEntry = Mock.Of<ICacheEntry>();

    var mockMemoryCache = Mock.Get(memoryCache);
    mockMemoryCache
        .Setup(m => m.CreateEntry(It.IsAny<object>()))
        .Returns(cachEntry);

    var cachedResponse = memoryCache.Set<string>(url, response);

    Assert.IsNotNull(cachedResponse);
    Assert.AreEqual(response, cachedResponse);
}

1
那是一个非常好的答案,谢谢。我现在正在尝试模拟IMemoryCache上的“set”方法。如果您可以查看我问题底部的编辑,我将不胜感激。 - BeniaminoBaggins
1
是的,就是这样。Moq。查看答案中链接的文档,你就会知道我从哪里得到它。 - Nkosi
1
@Omicron,你不需要移动任何东西。那段代码就是实际的源代码。它旨在演示扩展方法访问的内容,以便您知道要模拟什么。 - Nkosi
1
@Omicron 也可以做到这一点,我只是为了让代码更清晰并具有可重用性而这样做。 - Nkosi
1
@Omicron,你是否包含了 out 关键字? - Nkosi
显示剩余14条评论

10
正如welrocken所指出的那样,在您尝试模拟的接口中没有Get方法。Nkosi已经为扩展方法提供了源代码链接,这些扩展方法是大多数人将使用IMemoryCache的典型用法。基本上,所有的扩展方法在其执行过程中都会调用三个接口方法之一。
检查正在进行的操作的一种快速而粗略的方法是,在所有三个模拟接口方法上设置回调并打一个断点。
要特别模拟其中一个Get方法,假设您的测试目标方法调用了Get,那么您可以像这样模拟该结果:
    delegate void OutDelegate<TIn, TOut>(TIn input, out TOut output);

    [Test]
    public void TestMethod()
    {
        // Arrange
        var _mockMemoryCache = new Mock<IMemoryCache>();
        object whatever;
        _mockMemoryCache
            .Setup(mc => mc.TryGetValue(It.IsAny<object>(), out whatever))
            .Callback(new OutDelegate<object, object>((object k, out object v) =>
                v = new object())) // mocked value here (and/or breakpoint)
            .Returns(true); 

        // Act
        var result = _target.GetValueFromCache("key");

        // Assert
        // ...
    }

编辑: 我在这个答案中添加了一个模拟setter的示例。


7
如果您正在使用MemoryCacheEntryOptions和.AddExpirationToken进行调用,则还需要该条目具有令牌列表。
这是对@Nkosi上面回答的补充。 示例:
// cache by filename: https://jalukadev.blogspot.com/2017/06/cache-dependency-in-aspnet-core.html
var fileInfo = new FileInfo(filePath);
var fileProvider = new PhysicalFileProvider(fileInfo.DirectoryName);
var options = new MemoryCacheEntryOptions();
options.AddExpirationToken(fileProvider.Watch(fileInfo.Name));
this.memoryCache.Set(key, cacheValue, options);

模拟测试需要包括以下内容:
// https://github.com/aspnet/Caching/blob/45d42c26b75c2436f2e51f4af755c9ec58f62deb/src/Microsoft.Extensions.Caching.Memory/CacheEntry.cs
var cachEntry = Mock.Of<ICacheEntry>();
Mock.Get(cachEntry).SetupGet(c => c.ExpirationTokens).Returns(new List<IChangeToken>());

var mockMemoryCache = Mock.Get(memoryCache);
mockMemoryCache
    .Setup(m => m.CreateEntry(It.IsAny<object>()))
    .Returns(cachEntry);

0

Microsoft.Extensions.Caching.Memory.IMemoryCache 接口中没有 Microsoft.Extensions.Caching.Memory.IMemoryCache.Get(object) 方法。你尝试使用的方法在 Microsoft.Extensions.Caching.Memory.CacheExtensions 中。你可以查看以下答案来间接回答你的问题:

如何使用 Moq 模拟扩展方法?

使用 Moq 模拟扩展方法

你还应该知道如何配置 Moq 以供后续使用;

你的代码声明 Get 方法返回一个字符串,并且需要一个字符串参数。如果你的测试配置在整个测试过程中都遵循这个规则,那就没问题了。但是根据声明,Get 方法需要一个对象作为键。所以你的参数谓词代码应该是 It.IsAny<object>()

第二件事是,如果你想返回 null,你应该将其转换为函数实际返回的类型(例如 .Returns((string)null))。这是因为 Moq.Language.IReturns.Returns 还有其他重载,编译器无法确定你要引用哪一个。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接