4

I understand IMemoryCache.Set is an extension method so it can not be mocked. People have provided workarounds to such situation e.g as one by the NKosi here. I am wondering how I can achieve that for my data access layer where my MemoryCache returns a value and when not found it gets data from the db, set it to the MemoryCache and return the required value.

    public string GetMessage(int code)
    {
        if(myMemoryCache.Get("Key") != null)
        {
            var messages= myMemoryCache.Get<IEnumerable<MyModel>>("Key");
            return messages.Where(x => x.Code == code).FirstOrDefault().Message;
        }

        using (var connection = dbFactory.CreateConnection())
        {
            var cacheOptions = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(1) };
            const string sql = @"SELECT Code, Message FROM MyTable";

            var keyPairValueData = connection.Query<KeyPairValueData>(sql);

            myMemoryCache.Set("Key", keyPairValueData, cacheOptions );

            return keyPairValueData.Where(x => x.Code == code).FirstOrDefault().Message;
        }
    }

Following is my Unit Test - And off course it is not working as I can't mock IMemoryCache

    [Fact]
    public void GetMessage_ReturnsString()
    {
        //Arrange
        // Inserting some data here to the InMemoryDB

        var memoryCacheMock = new Mock<IMemoryCache>();

        //Act
        var result = new DataService(dbConnectionFactoryMock.Object, memoryCacheMock.Object).GetMessage(1000);

        //assert xunit
        Assert.Equal("Some message", result);
    }
Learning Curve
  • 1,449
  • 7
  • 30
  • 60
  • Trying to fully mock that interface is tricky because of all the extensions used. That is why I started suggesting the use if the actual memory cache in the linked answer. – Nkosi Nov 09 '18 at 11:30
  • What is the problem with the current test – Nkosi Nov 09 '18 at 12:20
  • Object reference not set to an instance of an object error at the myMemoryCache.Set("Key", keyPairValueData, cacheOptions ); – Learning Curve Nov 09 '18 at 12:45
  • And have you tried using the approach from the linked answer? – Nkosi Nov 09 '18 at 12:51
  • Creating a new instance of MemoryCache rather mocking works perfectly fine and I already had that done but wasn't sure if should be using actual objects rather mocks. So on that note first suggestion by @Kenneth works perfectly fine. Haven't tried his mocking suggestion yet though. – Learning Curve Nov 09 '18 at 13:07

1 Answers1

8

The first thing I would say is why not use a real memory cache? It would verify the behavior much better and there's no need to mock it:

// Arrange
var memCache = new MemoryCache("name", new NameValueCollection());

//Act
var result = new DataService(dbConnectionFactoryMock.Object, memCache).GetMessage(1000);

// Assert: has been added to cache
memCache.TryGetValue("Key", out var result2);
Assert.Equal("Some message", result2);

// Assert: value is returned
Assert.Equal("Some message", result);

If you really want to mock it out, here's a guide on how to do that:

Because it's an extension method, you need to make sure that it can be called as is. What happens in your case is that the extension method will call into the mock. Since you provide no expected behavior, it will probably fail.

You need to look at the code for the extension method, check what it accesses and then ensure that your mock complies with the expected behavior. The code is available here: https://github.com/aspnet/Caching/blob/master/src/Microsoft.Extensions.Caching.Abstractions/MemoryCacheExtensions.cs#L77

This is the code:

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options)
    {
        using (var entry = cache.CreateEntry(key))
        {
            if (options != null)
            {
                entry.SetOptions(options);
            }

            entry.Value = value;
        }

        return value;
    }

So, from that, you can see that it accesses CreateEntyand expects an object from it. Then it calls SetOptions and assigns Value on the entry.

You could mock it like this:

var entryMock = new Mock<ICacheEntry>();
memoryCacheMock.Setup(m => m.CreateEntry(It.IsAny<object>())
               .Returns(entryMock.Object);

// maybe not needed
entryMock.Setup(e => e.SetOptions(It.IsAny<MemoryCacheEntryOptions>())
         ...

When you do this, the extension method will be called on the mock and it will return the mocked entry. You can modify the implementation and make it do whatever you want.

Kenneth
  • 28,294
  • 6
  • 61
  • 84
  • I believe you meant to say why to use a real memory cache rather "why not"? Please correct me if I am wrong! – Learning Curve Nov 09 '18 at 11:14
  • No, I did mean "why not use a real memory cache" – Kenneth Nov 09 '18 at 11:27
  • @Kenneth I believe the first snippet example is off as MemoryCache does not have a parameterless constructor. – Nkosi Nov 09 '18 at 11:35
  • Ah, yeah, you're right. You need to change that to `.Returns(entryMock.Object)` because you want to return the instance, not the mock container. I've updated the answer – Kenneth Nov 09 '18 at 13:59
  • Thanks @Kenneth. In your first snippet, you have passed 2 parameters as Nkosi suggested. In my case, It only allows me to pass MemoryCacheOptions. var cache = new MemoryCache(new MemoryCacheOptions()); It works fine with the options parameter but just wondering what I am doing different here. Apart from this small change both approaches working fine for me. – Learning Curve Nov 09 '18 at 14:16
  • To be honest, I'm not entirely sure which specific version of MemoryCache you're using. I just looked up the docs and used the first signature I found. not important anyway, as long as it works. – Kenneth Nov 09 '18 at 14:26
  • I am trying to unit test the same scenario and after implementing the code i am getting "ICacheEntry does not contain a definition for 'Returns'" here: memoryCacheMock.Setup(m => m.CreateEntry(It.IsAny()) .Returns(entryMock.Object); – esmehsnj May 31 '19 at 13:02