如何对使用缓存的服务进行单元测试?

6

我有一个服务层,其中包含多个方法。这些方法已经实现了缓存,例如以下代码:

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

现在的问题是,我也想进行单元测试,如下所示:
[TestMethod]
public void GetCategories()
{
    IContentService contentService = new ContentService();
    var resp = contentService.GetCategories();
    Assert.IsNotNull(resp,"Should not be null");
}

问题是,在单元测试期间,我的CacheHandler中的HttpContext.Current为空(显然)。
最简单的解决方法是什么?
(请尽可能具体,因为我以前没有做过很多单元测试)
4个回答

8
这里需要使用依赖注入。我看到的主要问题是你静态地访问了CacheHandler,所以在单元测试中,你:
a) 不能测试服务而不“测试”CacheHandler
b) 不能向服务提供其他的CacheHandler,例如一个模拟的对象。

如果在你的情况下可能的话,我会重构或者至少包装CacheHandler,这样服务就可以访问它的实例。在单元测试中,你可以使用“伪造”的CacheHandler来提供服务,它不会访问HttpContext,并且还可以非常精细地控制测试本身(例如,在两个完全独立的单元测试中,你可以测试缓存项目和未缓存项目时发生的情况)。

对于模拟部分,我认为最简单的方法是创建一个接口,然后使用一些专门用于测试的自动模拟/代理生成框架,例如Rhino Mocks(但还有许多其他框架可供选择,我只是使用这个并且非常满意 :))。另一种方法(对于初学者来说更容易,但在实际开发中更加繁琐)是简单地设计CacheHandler(或其包装器),以便您可以继承它并自己覆盖行为。
最后,对于注入本身,我发现了一个方便的“模式”,它利用了C#默认方法参数和标准构造函数注入。服务构造函数将如下所示:
public ContentService(ICacheHandler cacheHandler = null)
{
    // Suppose I have a field of type ICacheHandler to store the handler
    _cacheHandler = cacheHandler ?? new CacheHandler(...);
}

在应用程序本身中,我可以调用没有参数的构造函数(或者让框架构建服务,如果它是ASP.NET处理程序、WCF服务或其他类型的类),在单元测试中,我可以提供实现该接口的任何内容。
在Rhino Mocks的情况下,它可能看起来像这样:
var mockCacheHandler = MockRepository.GenerateMock<ICacheHandler>();
// Here I can mock/stub methods and properties, set expectations etc...
var sut = new ContentService(mockCacheHandler);

1
成功了!完美!这真的帮了我很多 :) Rhino Mocks 也非常好。谢谢! - Lars Holdgaard

4
Honza Brestan所推荐的依赖注入是一种有效的解决方案,也许是最好的解决方案 - 特别是如果将来可能希望使用ASP.NET缓存之外的其他东西。
然而,我应该指出,您可以在不需要HttpContext的情况下使用ASP.NET Cache。 您可以使用静态属性HttpRuntime.Cache来引用它,而不是将其引用为HttpContext.Current.Cache。
这将使您能够在HTTP请求的上下文之外使用Cache,例如在单元测试或后台工作线程中。 实际上,我通常建议在业务层中使用HttpRuntime.Cache进行数据缓存,以避免对存在HttpContext的依赖。

3
将缓存分离成一个独立的类,像代理一样工作。
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"];
    }
}

为了启用缓存,将真正的ContentService与缓存一起使用:
var service = new CachingContentService(new ContentService());

为了测试缓存,请创建带有测试双倍构造函数参数的CachingContentService。使用测试双倍验证缓存:调用一次,它应该调用服务后面。调用两次,它不应该调用服务后面。


1
作为最佳实践,您只想在测试期间测试一件事情,而您所描述的有多个步骤。因此,最好构建您的“RequestHelper.MakeRequest”和其他例程的测试,使它们单独运行其测试,而不是与缓存场景一起运行。将它们分开测试将让您知道这些例程或缓存中是否存在问题。稍后可以将它们集成以作为一组进行测试。
要单独测试缓存,您可以创建一个模拟对象来创建具有所需属性的HttpContext。以下是一些先前的答案,应该可以帮助您将其组合在一起: 如何使用Moq在ASP.NET MVC中模拟HttpContext? 在Test Init方法中模拟HttpContext.Current

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接