I\'m trying to mock IMemoryCache
with Moq. I\'m getting this error:
An exception of type \'System.NotSupportedException\' occurred in M
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.
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.
According to source code for MemoryCacheExtensions.cs,
The Get<TItem>
extension method makes use of the following
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;
}
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), theout
parameter must be declared as anobject
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);
}
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);
}
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);