Search code examples
c#unit-testingasp.net-core.net-coremoq

Mock IMemoryCache with Moq throwing exception


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".


Solution

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