模拟IMemoryCache与Moq抛出异常

时间:2017-02-22 01:21:57

标签: c# .net unit-testing asp.net-core moq

我试图用Moq模仿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。但是错误的是&#34;对象引用没有设置为对象的实例&#34;。

4 个答案:

答案 0 :(得分:26)

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

来自评论

  

请注意,在模拟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);
}

答案 1 :(得分:2)

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

答案 2 :(得分:1)

如果您使用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);

答案 3 :(得分:0)

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

How do I use Moq to mock an extension method?

Mocking Extension Methods with Moq

您还应了解如何为以后的用法配置Moq;

您的代码声明Get方法返回一个字符串,并接受一个字符串参数。如果您的测试配置贯穿整个测试,那就没问题了。但是通过声明,Get方法将对象作为键。所以参数谓词的代码是It.IsAny<object>()

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