fanfan17
fanfan17

Reputation: 1

Unit tests for a cache layer in ASP.NET core Web api where the cache item comes from an outside url

I am very new to ASP.NET and I am trying to write xunit tests for a cache class. This cache class is used for get some data and return the data in a web api controller. The cached data comes from some outside urls, where each cache item has its own cache key.

In my cache.cs class, I have a method called GetCacheGetOrCreateAsync(... cacheDataSetting), and in my controller I will call it to get the data I want. I implemented the cache using IMemoryCache class.

There is also a private method in cache.cs called GetDataByteArrayAsync(string url), in which I create a httpclient from IHttpClientFactory (it been injected to the cache.cs class level), and returns the outside data get by httpclient.

GetCacheGetOrCreateAsync(... cacheDataSetting) calls GetDataByteArrayAsync(string url) to get data and then cache it.

Now I want to write unit tests for GetCacheGetOrCreateAsync(... cacheDataSetting) (since it's the only public method in this class), and I want to mock the IHttpClientFactory and httpclient, but I'm confused about how can I actually use the mocked httpclient return value since GetCacheGetOrCreateAsync(... cacheDataSetting) calls GetDataByteArrayAsync(string url) directly. Should I make httpclient a formal parameter for GetCacheGetOrCreateAsync(... cacheDataSetting) method? What will be a universal practice for unit testing the cache behaviors?

Thank you!

I have tried to mock the IHttpClientFactory and httpclient, also tried to directly mock IMemoryCache but also found someone suggested we cannot mock cache directly using Moq. Super confused.

Upvotes: 0

Views: 465

Answers (1)

Denis Kiryanov
Denis Kiryanov

Reputation: 871

Not sure about your architecture approach. Maybe, there should be some services responsible for the business logic? Then you can wrap the IMemoryCache and use it in the services:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;

public interface IMemoryCacheWrapper
{
    bool TryGetValue<T>(string key, out T value);

    void Set<T>(string key, T value, TimeSpan expirationTime);
}

public class MemoryCacheWrapper : IMemoryCacheWrapper
{
    private readonly IMemoryCache _memoryCache;

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

    public bool TryGetValue<T>(string key, out T value)
    {
        return _memoryCache.TryGetValue(key, out value);
    }

    public void Set<T>(string key, T value, TimeSpan expirationTime)
    {
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(expirationTime);

        MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(new CancellationChangeToken(cancellationTokenSource.Token));

        _memoryCache.Set(key, value, cacheEntryOptions);
    }
}

After this, let's inject the cache dependency into your service:

public class MyService : IMyService
{
    private readonly IMemoryCacheWrapper _memoryCacheWrapper;
    private readonly IDataRepository _repository;

    public MyService(IMemoryCacheWrapper memoryCacheWrapper, IDataRepository repository)
    {
        _memoryCacheWrapper = memoryCacheWrapper;
        _repository = repository;
    }

    public async Task<IEnumerable<ItemModel?>> GetCacheItemsAsync(FilterModel filter, CancellationToken token)
    {
        string cacheKey = $"{CacheSettings.CacheKeyPrefix}{filter.Date}";

        if (_memoryCacheWrapper.TryGetValue(cacheKey, out IEnumerable<ItemModel?> cacheResult))
        {
            return cacheResult;
        }

        IEnumerable<ItemModel?> result = _repository.GetSomeItemsAsync(filter.Date, token);

        _memoryCacheWrapper.Set(cacheKey, result, CacheSettings.CacheExpirationTime);

        return result;
    }
}

Now, you can use your services in controllers and easily mock them. Thus, probably there is no need to mock the IHttpClientFactory to test your contollers. Please provide the source code and I will try to get a better solution if there are still questions.

Upvotes: 0

Related Questions