【问题标题】:Mock IMemoryCache with Moq throwing exception模拟 IMemoryCache 与 Moq 抛出异常
【发布时间】:2017-07-11 21:13:20
【问题描述】:

我正在尝试使用 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&lt;String&gt;(url) 并让它返回null?

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

按照我尝试过的这个问题的答案:

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

因为在 memoryCache 扩展中它显示它在 Set 内使用 CreateEntry。但它出错了“对象引用未设置为对象的实例”。

【问题讨论】:

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


    【解决方案1】:

    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 方法返回一个字符串,并采用一个字符串参数。如果您的测试配置在整个测试中都遵循这一点,那就没问题了。但是通过声明,Get 方法将对象作为键。所以你的参数谓词代码是It.IsAny&lt;object&gt;()

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

    【讨论】:

      【解决方案2】:

      根据MemoryCacheExtensions.cs的源代码,

      Get&lt;TItem&gt; 扩展方法利用了以下内容

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

      来自 cmets

      请注意,在模拟 TryGetValue(代替 Get)时,out 参数 必须声明为object,即使不是。

      例如:

      int expectedNumber = 1; 
      object expectedValue = expectedNumber. 
      

      如果您不这样做,那么它将匹配 同名。

      这是一个使用修改后的服务的示例,如何模拟memoryCache.Get&lt;String&gt;(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&lt;&gt;扩展方法,看起来像这样。

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

      【讨论】:

      • 这是一个非常好的答案,谢谢。我现在正在尝试在 IMemoryCache 上模拟 set 方法。如果您能在我的问题底部查看编辑,将不胜感激。
      • 是的。这就是起订量。检查答案中链接的文档,您会看到我从哪里得到的。
      • @Omicron 你什么都不动。该代码是实际的源代码。这是为了演示扩展方法访问的内容,以便您知道要模拟什么。
      • @Omicron 也可以做到这一点,我只是这样做是为了使代码干净并具有可重用的实用程序。
      • @Omicron,您是否包含了out 关键字?
      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-04-09
          • 1970-01-01
          • 2019-07-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多