使用最小起订量抛出异常模拟 IMemoryCache

Mock IMemoryCache with Moq throwing exception

我正在尝试使用最小起订量模拟 IMemoryCache。我收到此错误:

An exception of type 'System.NotSupportedException' occurred in Moq.dll but was not handled in user code

Additional information: Expression references a method that does not belong to the mocked object: 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) 并让它 return 为空?

编辑:如果不是 _memoryCache.Set<String>(url, response);,我将如何做同样的事情?我不介意它 returns,我只需要将方法添加到模拟中,这样它在调用时就不会抛出。

我试过这个问题的答案:

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

因为在 memoryCache 扩展中它显示它在 Set 中使用 CreateEntry。但是 "object reference not set to an instance of an object".

出错了

Microsoft.Extensions.Caching.Memory.IMemoryCache接口中没有Microsoft.Extensions.Caching.Memory.IMemoryCache.Get(object)方法。您尝试使用的那个在 Microsoft.Extensions.Caching.Memory.CacheExtensions 中。您可以查看这些答案以间接回答您的问题;

How do I use Moq to mock an extension method?

Mocking Extension Methods with Moq.

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

您的代码声明 Get 方法 return 是一个字符串,并采用一个字符串参数。如果您的测试配置在整个测试过程中都遵循该配置,那很好。但是通过声明,Get 方法将对象作为键。所以你的参数谓词代码将是 It.IsAny<object>()

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

根据 MemoryCacheExtensions.cs

的源代码

Get<TItem> 扩展方法使用以下

public static TItem Get<TItem>(this IMemoryCache cache, object key) {
    TItem value;
    cache.TryGetValue<TItem>(key, out value);
    return value;
}

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

    value = default(TItem);
    return false;
}

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

鉴于用 Moq 模拟扩展方法是不可行的,尝试模拟扩展方法访问的接口成员。

参考 Moq's quickstart 更新 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) 并让它 return 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 方法,该方法 return 是一个 ICacheEntry 也被执行。因此,将模拟设置为 return 一个模拟条目,就像下面的示例一样

[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);
}

如果您使用 MemoryCacheEntryOptions and .AddExpirationToken 调用 Set,那么您还需要该条目以包含令牌列表。

这是对@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);

正如 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 的示例