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

本文关键字:服务 单元测试 缓存 | 更新日期: 2023-09-27 18:26:26

我有一个服务层,它有一系列方法。这些方法实现了缓存,如下所示:

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在单元测试期间为空(显然)。

解决这个问题最简单的方法是什么?

(请尽可能具体,因为我以前没有做过很多单元测试)

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

这尖叫着依赖注入。我看到的主要问题是静态访问CacheHandler,因此在单元测试中,您:
a) 如果不"测试"CacheHandler,则无法测试服务
b) 不能向服务提供任何其他CacheHandler,例如模拟一个

如果在你的情况下可能的话,我会重构或至少包装CacheHandler,这样服务就可以访问它的一个实例。在单元测试中,你可以为服务提供一个"假的"CacheHandler,它不会访问HttpContext,也可以让你对测试本身有很好的控制(例如,你可以测试当一个项目被缓存时会发生什么,而不是当它不在两个绝对独立的单元测试中时)

对于mocking部分,我认为最简单的方法是创建一个接口,然后使用一些为测试设计的自动锁定/代理生成框架,例如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);

Honza Brestan的回答中建议的依赖注入无疑是一个有效的解决方案,也许是最好的解决方案——尤其是如果您将来可能想使用ASP.NET缓存之外的其他东西。

但是,我应该指出,您可以使用ASP.NET缓存,而不需要HttpContext。您可以使用静态属性HttpRuntime.Cache.,而不是将其引用为HttpContext.Current.Cache

这将使您能够在HTTP请求的上下文之外使用Cache,例如在单元测试或后台工作线程中。事实上,我通常建议在业务层中使用HttpRuntime.Cache进行数据缓存,以避免依赖于HttpContext的存在。

将缓存分离到自己的类中,就像代理一样工作。

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

若要测试缓存,请使用test double作为构造函数参数创建CachingContentService。使用test double来验证缓存:只调用一次,它就应该调用后面的服务。给它打两次电话,它不应该给后面的服务打电话。

作为一种最佳实践,您只想在测试中测试一件事,而您所描述的有多个步骤。因此,最好构建"RequestHelper.MakeRequest"和其他例程的测试,以便它们与缓存场景分开运行测试。单独测试它们可以让您知道它们在这些例程或缓存中是否存在问题。您可以稍后对它们进行集成,以将它们作为一个组进行测试。

要单独测试缓存,可以创建一个mock对象来创建具有所需属性的HttpContext。以下是一些以前的答案,应该可以帮助你把它放在一起:

如何使用Moq模拟ASP.NET MVC中的HttpContext?

Mock HttpContext.Current in Test Init Method