我有一个服务层,它有一系列方法。这些方法实现了缓存,如下所示:
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");
}
问题是,我的HttpContext.Current
中的CacheHandler
在单元测试期间(显然)为空。
解决此问题的最简单方法是什么?
(请尽可能具体,因为我之前没有做过很多单元测试)
答案 0 :(得分:4)
这会尖叫 dependency injection 。我看到的主要问题是您静态访问CacheHandler
,因此在单元测试中,您:
a)如果没有“测试”CacheHandler
,也无法测试服务
b)无法向服务提供任何其他CacheHandler
,例如 mocked 一个
如果在您的情况下这是可能的,我要么重构或至少包装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 :(得分:4)
根据Honza Brestan的回答推荐的依赖注入肯定是一个有效的解决方案,也许是最好的解决方案 - 特别是如果您将来可能想要使用除ASP.NET缓存以外的其他东西。
但是我应该指出,您可以使用ASP.NET缓存而无需HttpContext
。您可以使用静态属性HttpRuntime.Cache。
HttpContext.Current.Cache
这将使您能够在HTTP请求的上下文之外使用缓存,例如在单元测试或后台工作线程中。事实上,我通常建议在业务层中使用HttpRuntime.Cache
进行数据缓存,以避免依赖于HttpContext
的存在。
答案 2 :(得分: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());
要测试缓存,请使用test double作为构造函数参数创建CachingContentService
。使用test double验证缓存:调用一次,它应该调用后面的服务。调用它两次,它不应该调用后面的服务。
答案 3 :(得分:1)
作为最佳实践,您只想在测试期间测试一件事,并且您所描述的内容包含多个步骤。因此,最好构建对“RequestHelper.MakeRequest”和其他例程的测试,以便它们与缓存场景分开运行测试。单独测试它们会让您知道它们在这些例程或缓存中是否存在问题。您可以稍后将它们集成以作为一个组进行测试。
要单独测试缓存,可以创建一个模拟对象来创建具有所需属性的HttpContext。以下是一些以前的答案,可以帮助你把它们放在一起: