带有最小起订量抛出异常的模拟 IMemoryCache

问题描述 投票:0回答:4

我正在尝试用最小起订量来模拟

IMemoryCache
。我收到这个错误:

“System.NotSupportedException”类型的异常发生在 Moq.dll 但未在用户代码中处理

附加信息:表达式引用的方法不 属于模拟对象:x => x.Get(It.IsAny())

我的模拟代码:

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
。但它因“对象引用未设置到对象的实例”而出错。

c# unit-testing asp.net-core .net-core moq
4个回答
75
投票

根据 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;
    }
}

来自评论

请注意,在模拟

TryGetValue
(代替 Get)时,
out
参数 必须声明为
object
,即使它不是。

例如:

int expectedNumber = 1; 
object expectedValue = expectedNumber. 

如果你不这样做,那么它将匹配一个模板化的扩展方法 同名

这里是一个使用修改后的服务的例子,如何模拟

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

8
投票

正如 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
        // ...
    }

编辑: 我在this answer中添加了一个关于如何模拟setter的例子。


7
投票

如果您使用

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

0
投票

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

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

Moq 模拟扩展方法.

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

您的代码声明 Get 方法返回一个字符串,并接受一个字符串参数。如果您的测试配置在整个测试过程中都遵循该配置,那很好。但是通过声明,Get 方法将对象作为键。所以你的参数谓词代码是

It.IsAny<object>()

第二件事是,如果你想返回 null,你应该将它转换为你的函数实际返回的类型(例如

.Returns((string)null)
)。这是因为 Moq.Language.IReturns.Returns 还有其他重载,编译器无法决定您要引用哪一个。

© www.soinside.com 2019 - 2024. All rights reserved.