47

I'm trying to mock IMemoryCache with Moq. I'm getting this error:

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

My mocking code:

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

Why do I get that error?

This is the code under test:

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

Where _memoryCache is of type IMemoryCache

How do I mock the _memoryCache.Get<String>(url) above and let it return null?

Edit: How would I do the same thing but for _memoryCache.Set<String>(url, response);? I don't mind what it returns, I just need to add the method to the mock so it doesn't throw when it is called.

Going by the answer for this question I tried:

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

Because in the memoryCache extensions it shows that it uses CreateEntry inside Set. But it is erroring out with "object reference not set to an instance of an object".

Nkosi
  • 235,767
  • 35
  • 427
  • 472
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287

4 Answers4

84

According to source code for MemoryCacheExtensions.cs,

The Get<TItem> extension method makes use of the following

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

Notice that essentially it is using the TryGetValue(Object, out Object) method.

Given that it is not feasible to mock extension methods with Moq, Try mocking the interface members that are accessed by the extension methods.

Referring to Moq's quickstart update MockMemoryCacheService to properly setup the TryGetValue method for the test.

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

From comments

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.

Here is an example using the modified service of how to mock the memoryCache.Get<String>(url) and let it 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);
}

UPDATE

The same process can be applied for the Set<> extension method which looks like this.

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

    return value;
}

This method makes use of the CreateEntry method which returns a ICacheEntry which is also acted upon. So set up the mock to return a mocked entry as well like in the following example

[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);
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 1
    That was a really good answer thanks. I'm trying to mock the `set` method on IMemoryCache now. Would really appreciate if you could view the edit down the bottom of my question. – BeniaminoBaggins Feb 22 '17 at 23:41
  • @BeniaminoBaggins hold on let me take a look – Nkosi Feb 22 '17 at 23:44
  • Thanks. It is called like this: `_memoryCache.Set(url, response);` where `url` is a string and `response` is a json string. – BeniaminoBaggins Feb 23 '17 at 00:01
  • @BeniaminoBaggins you get null because you setup the cache to return null when it has to set values on the returned entry before returning from the method. drafting an update now – Nkosi Feb 23 '17 at 00:06
  • 1
    Yes. that is all Moq. Check the documentation linked in the answer and you will see where I got it from. – Nkosi Feb 23 '17 at 00:17
  • Why does moving the TryGetValue method to a static class fix the issue? – Omicron Jul 05 '17 at 15:52
  • 1
    @Omicron you do not move anything. That code is the actual source code. It was to demonstrate what the extension method accesses so that you know what to mock. – Nkosi Jul 05 '17 at 15:53
  • In your comment you create a static class called MockMemoryCacheService and then setup the TryGetValue method inside that. What I was asking is why can't I just setup the TryGetValue method inside my test method instead of having to make the static class? – Omicron Jul 05 '17 at 15:57
  • 1
    @Omicron can do that as well I just did that to make the code clean and have a reusable utility. – Nkosi Jul 05 '17 at 16:02
  • When I try setting up the object in the test method I get the following error: "System.NotSupportedException : Expression references a method that does not belong to the mocked object: c => c.TryGetValue(It.IsAny(), .expectedModelNumber)" – Omicron Jul 05 '17 at 16:11
  • 1
    @Omicron, did you include the `out` key word? – Nkosi Jul 05 '17 at 16:14
  • Here's the statement in the code: "mockedMemoryCache.Setup(c => c.TryGetValue(It.IsAny(), out expectedModelNumber)).Returns(true);" – Omicron Jul 05 '17 at 16:16
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148432/discussion-between-nkosi-and-omicron). – Nkosi Jul 05 '17 at 16:20
  • 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. – Leo Hendry Nov 30 '17 at 12:56
  • @LeoHendry Thanks for the info. I believe that is how I showed it in the answer. I will update to include your warning so that it is clearer. – Nkosi Nov 30 '17 at 13:03
  • I have tried this solution and it is working fine while setting cache but if only want to create a mock for get method then I am getting null. Like object value = productData; var mockCache = new Mock(); mockCache .Setup(x => x.TryGetValue( It.Is(a => a.Equals(cacheKey)), out value )) .Returns(true); return mockCache; and then when it is calling cache.Get(cacheKey) I am getting null – Ask Nov 01 '18 at 11:11
  • productData is a ProductModel btw – Ask Nov 01 '18 at 11:13
  • @Ask Try with `It.IsAny()` first to see if it works. If it does then the problem is with your filter in the `It.Is` – Nkosi Nov 01 '18 at 11:13
  • @Nkosi, I have asked a new question to clarify my problem. can you look into that? https://stackoverflow.com/questions/53100873/problem-in-casting-generic-list-to-system-threading-task-generic-list – Ask Nov 01 '18 at 12:01
9

As pointed out by welrocken, there is no Get method in the interface you're trying to mock. Nkosi has helpfully linked the source code for the extension methods which are the typical usages most people will make of the IMemoryCache. Fundamentally, all of the extension methods call one of the three interface methods somewhere in their execution.

A quick and dirty way of inspecting what's going on is to setup a callback on all three of the mocked interface methods and stick a breakpoint in.

To specifically mock one of the Get methods, assuming that your test target method is calling Get, then you can mock that result like this:

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

EDIT: I've added an example on how to mock the setter in this answer.

user1007074
  • 2,093
  • 1
  • 18
  • 22
7

If you're calling the Set with a MemoryCacheEntryOptions and .AddExpirationToken, then you'll also need the entry to have a list of tokens.

This is an addition to @Nkosi's answer above. Example:

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

The mock needs to include:

// 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);
AlignedDev
  • 8,102
  • 9
  • 56
  • 91
0

There is no Microsoft.Extensions.Caching.Memory.IMemoryCache.Get(object) method in the Microsoft.Extensions.Caching.Memory.IMemoryCache interface. The one you try to use is in the Microsoft.Extensions.Caching.Memory.CacheExtensions. You can look at these answers to in-directly answer your question;

How do I use Moq to mock an extension method?

Mocking Extension Methods with Moq.

You should also be aware of how to configure Moq for later usages;

Your code states that the Get method returns a string, and takes a string parameter. If your test configuration follows that through out the whole test, it is fine. But by declaration, the Get method takes an object as a key. So your code for parameter predicate would be It.IsAny<object>().

The second thing is, if you want to return null, you should cast that to the type your function actually returns (e.g .Returns((string)null)). This is because there are other overloads for Moq.Language.IReturns.Returns, and the compiler cannot decide which one you are trying to refer to.

Community
  • 1
  • 1
welrocken
  • 266
  • 1
  • 9