Bill Posters
Bill Posters

Reputation: 1219

Mock IMemoryCache in unit test

I am using asp net core 1.0 and xunit.

I am trying to write a unit test for some code that uses IMemoryCache. However whenever I try to set a value in the IMemoryCache I get an Null reference error.

My unit test code is like this:
The IMemoryCache is injected into the class I want to test. However when I try to set a value in the cache in the test I get a null reference.

public Test GetSystemUnderTest()
{
    var mockCache = new Mock<IMemoryCache>();

    return new Test(mockCache.Object);
}

[Fact]
public void TestCache()
{
    var sut = GetSystemUnderTest();

    sut.SetCache("key", "value"); //NULL Reference thrown here
}

And this is the class Test...

public class Test
{
    private readonly IMemoryCache _memoryCache;
    public Test(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void SetCache(string key, string value)
    {
        _memoryCache.Set(key, value, new MemoryCacheEntryOptions {SlidingExpiration = TimeSpan.FromHours(1)});
    }
}

My question is...Do I need to setup the IMemoryCache somehow? Set a value for the DefaultValue? When IMemoryCache is Mocked what is the default value?

Upvotes: 61

Views: 59913

Answers (7)

Abdullah Tahan
Abdullah Tahan

Reputation: 2119

I solved that by creating a wrapper class

public interface IMemoryCacheService<TKey, TValue>
{       
      TValue AddOrUpdate(TKey key, TValue value, TimeSpan expiration);
      TValue TryGetValue(TKey key);
}
public class MemoryCacheService<TKey, TValue> : IMemoryCacheService<TKey, TValue>
{

    private readonly IMemoryCache _cache;

    private readonly object _lock = new object();

    public MemoryCacheService(IMemoryCache cache)
    {
        if (cache == null)
        {
            throw new ArgumentNullException(nameof(cache));
        }

        _cache = cache;
    }

    public TValue AddOrUpdate(TKey key, TValue value, TimeSpan expiration)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        lock (_lock)
        {
            return _cache.Set(key, value, expiration);
        }
    }
    public TValue TryGetValue(TKey key)
    {
        if (_cache.TryGetValue(key, out TValue value))
        {
            return value;
        }

        return default;
    }
}

Than in my unit testing class

 private readonly IMemoryCacheService<string, MyType> _memoryCacheService = A.Fake<IMemoryCacheService<string, MyType>>();

Finally in the test

    [Fact]
    public async Task GetCache_ShouldMockCaching_GivenValidCache()
    {
        A.CallTo(() => _memoryCacheService.TryGetValue(A<string>._)).ReturnsNextFromSequence(myMockupsCaching.ToArray());
    }

Upvotes: 0

user1007074
user1007074

Reputation: 2586

TLDR

Scroll down to the code snippet to mock the cache setter indirectly (with a different expiry property)

/TLDR

While it's true that extension methods can't be mocked directly using Moq or most other mocking frameworks, often they can be mocked indirectly - and this is certainly the case for those built around IMemoryCache

As I have pointed out in this answer, fundamentally, all of the extension methods call one of the three interface methods somewhere in their execution.

Nkosi's answer raises very valid points: it can get complicated very quickly and you can use a concrete implementation to test things. This is a perfectly valid approach to use. However, strictly speaking, if you go down this path, your tests will depend on the implementation of third party code. In theory, it's possible that changes to this will break your test(s) - in this situation, this is highly unlikely to happen because the caching repository has been archived.

Furthermore there is the possibility that using a concrete implementation with a bunch of dependencies might involve a lot of overheads. If you're creating a clean set of dependencies each time and you have many tests this could add quite a load to your build server (I'm not saying that that's the case here, it would depend on a number of factors)

Finally you lose one other benefit: by investigating the source code yourself in order to mock the right things, you're more likely to learn about how the library you're using works. Consequently, you might learn how to use it better and you will almost certainly learn other things.

For the extension method you are calling, you should only need three setup calls with callbacks to assert on the invocation arguments. This might not be appropriate for you, depending on what you're trying to test.

[Fact]
public void TestMethod()
{
    var expectedKey = "expectedKey";
    var expectedValue = "expectedValue";
    var expectedMilliseconds = 100;
    var mockCache = new Mock<IMemoryCache>();
    var mockCacheEntry = new Mock<ICacheEntry>();

    string? keyPayload = null;
    mockCache
        .Setup(mc => mc.CreateEntry(It.IsAny<object>()))
        .Callback((object k) => keyPayload = (string)k)
        .Returns(mockCacheEntry.Object); // this should address your null reference exception

    object? valuePayload = null;
    mockCacheEntry
        .SetupSet(mce => mce.Value = It.IsAny<object>())
        .Callback<object>(v => valuePayload = v);

    TimeSpan? expirationPayload = null;
    mockCacheEntry
        .SetupSet(mce => mce.AbsoluteExpirationRelativeToNow = It.IsAny<TimeSpan?>())
        .Callback<TimeSpan?>(dto => expirationPayload = dto);

    // Act
    var success = _target.SetCacheValue(expectedKey, expectedValue,
        new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMilliseconds(expectedMilliseconds)));

    // Assert
    Assert.True(success);
    Assert.Equal(expectedKey, keyPayload);
    Assert.Equal(expectedValue, valuePayload as string);
    Assert.Equal(TimeSpan.FromMilliseconds(expectedMilliseconds), expirationPayload);
}

Upvotes: 14

Marco Merola
Marco Merola

Reputation: 879

public sealed class NullMemoryCache : IMemoryCache
{
    public ICacheEntry CreateEntry(object key)
    {
        return new NullCacheEntry() { Key = key };
    }

    public void Dispose()
    {            
    }

    public void Remove(object key)
    {
        
    }

    public bool TryGetValue(object key, out object value)
    {
        value = null;
        return false;
    }

    private sealed class NullCacheEntry : ICacheEntry
    {
        public DateTimeOffset? AbsoluteExpiration { get; set; }
        public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }

        public IList<IChangeToken> ExpirationTokens { get; set; }

        public object Key { get; set; }

        public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }

        public CacheItemPriority Priority { get; set; }
        public long? Size { get; set; }
        public TimeSpan? SlidingExpiration { get; set; }
        public object Value { get; set; }

        public void Dispose()
        {
            
        }
    }
}

Upvotes: 15

Carpentweet
Carpentweet

Reputation: 349

I also came across this problem in a .Net 5 project and I solved it by wrapping the memory cache and only exposing the functionality that I need. This way I conform to the ISP and it's easier to work with my unit tests.

I created an interface

public interface IMemoryCacheWrapper
{
    bool TryGetValue<T>(string Key, out T cache);
    void Set<T>(string key, T cache);
}

Implemented the memory cache logic in my wrapper class, using MS dependency injection, so I'm not reliant on those implementation details in my class under test, plus it has the added benefit of adhering to the SRP.

public class MemoryCacheWrapper : IMemoryCacheWrapper
{
    private readonly IMemoryCache _memoryCache;

    public MemoryCacheWrapper(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void Set<T>(string key, T cache)
    {
        _memoryCache.Set(key, cache);
    }

    public bool TryGetValue<T>(string Key, out T cache)
    {
        if (_memoryCache.TryGetValue(Key, out T cachedItem))
        {
            cache = cachedItem;
            return true;
        }
        cache = default(T);
        return false;
    }
}

I added my memory cache wrapper to the dependency injection and I replaced the system memory cache in my code with the wrapper and that is what I mock out in my tests. All in all a relatively quick job and I think a better structure too.

In my test I then added this so that it mimics the cache updating.

        _memoryCacheWrapperMock = new Mock<IMemoryCacheWrapper>();
        _memoryCacheWrapperMock.Setup(s => s.Set(It.IsAny<string>(), It.IsAny<IEnumerable<IClientSettingsDto>>()))
            .Callback<string, IEnumerable<IClientSettingsDto>>((key, cache) =>
            {
                _memoryCacheWrapperMock.Setup(s => s.TryGetValue(key, out cache))
                    .Returns(true);
            });

Upvotes: 3

Ayushmati
Ayushmati

Reputation: 1592

This can be done by mocking the TryGetValue method for IMemoryCache instead of the Set method (Which as mentioned is an extension method and thus cannot be mocked).

  var mockMemoryCache = Substitute.For<IMemoryCache>();
  mockMemoryCache.TryGetValue(Arg.Is<string>(x => x.Equals(key)), out string expectedValue)
                .Returns(x =>
                {
                    x[1] = value;
                    return true;
                });

  var converter = new sut(mockMemoryCache);

Upvotes: -1

Nkosi
Nkosi

Reputation: 247008

IMemoryCache.Set Is an extension method and thus cannot be mocked using Moq framework.

The code for the extension though is available here

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

For the test, a safe path would need to be mocked through the extension method to allow it to flow to completion. Within Set it also calls extension methods on the cache entry, so that will also have to be catered for. This can get complicated very quickly so I would suggest using a concrete implementation

//...
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
//...

public Test GetSystemUnderTest() {
    var services = new ServiceCollection();
    services.AddMemoryCache();
    var serviceProvider = services.BuildServiceProvider();

    var memoryCache = serviceProvider.GetService<IMemoryCache>();
    return new Test(memoryCache);
}

[Fact]
public void TestCache() {
    //Arrange
    var sut = GetSystemUnderTest();

    //Act
    sut.SetCache("key", "value");

    //Assert
    //...
}

So now you have access to a fully functional memory cache.

Upvotes: 63

jenson-button-event
jenson-button-event

Reputation: 18941

I had a similar issue but I want to disable caching for debugging occasionally as its a pain to keep having to clear the cache. Just mock/fake them yourself (using StructureMap dependency injection).

You could easily use them in you tests as well.

public class DefaultRegistry: Registry
{
    public static IConfiguration Configuration = new ConfigurationBuilder()
        .SetBasePath(HttpRuntime.AppDomainAppPath)
        .AddJsonFile("appsettings.json")
        .Build();

    public DefaultRegistry()
    {
        For<IConfiguration>().Use(() => Configuration);  

#if DEBUG && DISABLE_CACHE <-- compiler directives
        For<IMemoryCache>().Use(
            () => new MemoryCacheFake()
        ).Singleton();
#else
        var memoryCacheOptions = new MemoryCacheOptions();
        For<IMemoryCache>().Use(
            () => new MemoryCache(Options.Create(memoryCacheOptions))
        ).Singleton();
#endif
        For<SKiNDbContext>().Use(() => new SKiNDbContextFactory().CreateDbContext(Configuration));

        Scan(scan =>
        {
            scan.TheCallingAssembly();
            scan.WithDefaultConventions();
            scan.LookForRegistries();
        });
    }
}

public class MemoryCacheFake : IMemoryCache
{
    public ICacheEntry CreateEntry(object key)
    {
        return new CacheEntryFake { Key = key };
    }

    public void Dispose()
    {

    }

    public void Remove(object key)
    {

    }

    public bool TryGetValue(object key, out object value)
    {
        value = null;
        return false;
    }
}

public class CacheEntryFake : ICacheEntry
{
    public object Key {get; set;}

    public object Value { get; set; }
    public DateTimeOffset? AbsoluteExpiration { get; set; }
    public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
    public TimeSpan? SlidingExpiration { get; set; }

    public IList<IChangeToken> ExpirationTokens { get; set; }

    public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; set; }

    public CacheItemPriority Priority { get; set; }
    public long? Size { get; set; }

    public void Dispose()
    {

    }
}

Upvotes: 4

Related Questions