How to unit test a service which uses caching?

11,501

Solution 1

This screams dependency injection. The main problem I see is that you access the CacheHandler statically, so in a unit test, you:
a) cannot test the service without "testing" the CacheHandler as well
b) cannot supply any other CacheHandler to the service, for example a mocked one

If that's possible in your case, I'd either refactor or at least wrap the CacheHandler so that the service accesses an instance of it. In a unit test, you can then supply the service with a "fake" CacheHandler, that would not access HttpContext and also could give you a very fine control over the test itself (e.g. you can test what happens when an item is cached vs. when it isn't in two absolutely independent unit tests)

For the mocking part, I suppose it's easiest to create an interface and then use some automocking/proxy-generation framework designed for testing, for example Rhino Mocks (but there are many more, it just happens that I'm using this one and am very happy with it :)). Another approach (easier for a beginner, but more cumbersome in an actual development) would be simply to design the CacheHandler (or its wrapper) so that you can inherit from it and override the behaviour yourself.

Finally for the injection itself, I have found out a handy "pattern", which takes advantage of C# default method arguments and the standard constructor injection. The service constructor would look like:

public ContentService(ICacheHandler cacheHandler = null)
{
    // Suppose I have a field of type ICacheHandler to store the handler
    _cacheHandler = cacheHandler ?? new CacheHandler(...);
}

So in the application itself, I can call the constructor without parameters (or let frameworks construct the service, if it's ASP.NET handler, WCF service or some other kind of class) and in unit tests, I can supply whatever is implementing the said interface.

In case of Rhino Mocks, it can look like this:

var mockCacheHandler = MockRepository.GenerateMock<ICacheHandler>();
// Here I can mock/stub methods and properties, set expectations etc...
var sut = new ContentService(mockCacheHandler);

Solution 2

Dependency Injection as recommended in Honza Brestan's answer is certainly a valid solution, and maybe the best solution - especially if you might want to use something other than the ASP.NET Cache in the future.

However I should point out that you can use the ASP.NET Cache without needing an HttpContext. Instead of referencing it as HttpContext.Current.Cache, you can use the static property HttpRuntime.Cache.

This will enable you to use the Cache outside the context of an HTTP request, such as in a unit test or in a background worker thread. In fact I'd generally recommend using HttpRuntime.Cache for data caching in the business tier to avoid taking the dependency on the existence of an HttpContext.

Solution 3

Separate the cache into its own class, working like a proxy.

public interface IContentService
{
    Categories GetCategories();
}

public class CachingContentService : IContentService
{
    private readonly IContentService _inner;

    public CachingContentSerice(IContentService _inner)
    {
        _inner = inner;
    }

    public Categories GetCategories()
    {
        string key = "GetCategories";
        if (!CacheHandler.ContainsKey(key))
        {
            Catogories categories = _inner.GetCategories();
            CacheHandler.InsertToCache(key, categories);
        }
        return CacheHandler.GetCache(key);
    }
}

public class ContentSerice : IContentService
{
    public Categories GetCategories()
    {
        return RequestHelper.MakeRequest("get_category_index")["categories"];
    }
}

To enable caching, decorate the real ContentService with the cache:

var service = new CachingContentService(new ContentService());

To test the cache, create the CachingContentService with a test double as a constructor parameter. Use the test double to verify the cache: call it once and it should call the service behind. Call it twice and it should not call the service behind.

Solution 4

As a best practice, you only want to test one thing during a test and what you're describing has multiple steps. Therefore, it would be best to construct your test of your "RequestHelper.MakeRequest" and other routines so that they run their tests separately from the caching scenario. Testing them separately will let you know if their are problems in these routines or in the caching. You can integrate them later to test them as a group.

To test the caching separately, you can create a mock object to create the HttpContext with the properties that you need. Here are some previous answers that should help you put this together:

How do I mock the HttpContext in ASP.NET MVC using Moq?

Mock HttpContext.Current in Test Init Method

Share:
11,501
Lars Holdgaard
Author by

Lars Holdgaard

An entrepreneur since I was 18. Been building a lot of stuff: some successful, some not so successful Currently building the best debt collection company in the world at Likvido. In my free time (when not tinkering with code), I'm very much into crypto, traveling and being digital-nomad when it suits my lifestyle... And I blog at LarsHoldgaard.com!

Updated on July 28, 2022

Comments

  • Lars Holdgaard
    Lars Holdgaard almost 2 years

    I have a service layer, which has a range of methods. These methods have implemented caching, like the following:

    string key = "GetCategories";
    if (CacheHandler.IsCachingEnabled() && !CacheHandler.ContainsKey(key))
    {
        var categories = RequestHelper.MakeRequest("get_category_index")["categories"];
        var converted = categories.ToObject<List<Category>>();
        CacheHandler.InsertToCache(key,converted);
        return converted;
    }
    return CacheHandler.GetCache(key) as List<Category>;
    

    Now, problem is I also want to make a unit test, like the following:

    [TestMethod]
    public void GetCategories()
    {
        IContentService contentService = new ContentService();
        var resp = contentService.GetCategories();
        Assert.IsNotNull(resp,"Should not be null");
    }
    

    Problem is, that the HttpContext.Current inside my CacheHandler is null during the unit test (obviously).

    What is the easiest way to fix this?

    (Please be as specific as possible because I haven't done a lot of unit testing before)