大量使用缓存的单元测试方法的最佳做法?


17

我有许多业务逻辑方法,用于存储和检索(通过过滤)对象以及来自缓存的对象列表。

考虑

IList<TObject> AllFromCache() { ... }

TObject FetchById(guid id) { ... }

IList<TObject> FilterByPropertry(int property) { ... }

Fetch..Filter..会调用AllFromCache它来填充缓存,如果不存在则返回,如果存在则从其返回。

我通常回避对这些单元进行测试。针对此类结构进行单元测试的最佳实践是什么?

我考虑过在TestInitialize上填充缓存,并在TestCleanup上删除缓存,但这对我来说并不对,(很可能)。

Answers:


18

如果要使用真正的单元测试,则必须模拟缓存:编写一个模拟对象,该对象实现与缓存相同的接口,但它不是缓存,而是跟踪收到的调用,并始终返回真实的内容。缓存应根据测试用例返回。

当然,缓存本身也需要进行单元测试,您必须对其进行模拟,以此类推。

您描述的使用实际缓存对象,但将其初始化为已知状态并在测试后清除的内容更像是集成测试,因为您正在同时测试多个单元。


+1这绝对是最好的方法。进行单元测试以检查逻辑,然后进行集成测试以实际验证缓存是否按预期工作。
汤姆·斯奎斯

10

单一职责原则在这里你最好的朋友。

首先,将AllFromCache()移至存储库类,并将其命名为GetAll()。它从缓存中检索的是存储库的实现详细信息,调用代码不应知道该信息。

这使测试您的过滤器类变得轻松而简单。它不再关心您从哪里得到它。

其次,将从数据库(或任何地方)获取数据的类包装在缓存包装器中。

AOP是一个很好的技术。这是它非常擅长的少数事情之一。

使用类似的工具 PostSharp之类的,您可以对其进行设置,以便所有标记有selected属性的方法都将被缓存。但是,如果这仅是您要缓存的内容,则无需深入了解AOP框架。只需拥有使用相同接口的存储库和缓存包装器,然后将其注入到调用类中即可。

例如。

public class ProductManager
{
    private IProductRepository ProductRepository { get; set; }

    public ProductManager
    {
        ProductRepository = productRepository;
    }

    Product FetchById(guid id) { ... }

    IList<Product> FilterByPropertry(int property) { ... }
}

public interface IProductRepository
{
    IList<Product> GetAll();
}

public class SqlProductRepository : IProductRepository
{
    public IList<Product> GetAll()
    {
        // DB Connection, fetch
    }
}

public class CachedProductRepository : IProductRepository
{
    private IProductRepository ProductRepository { get; set; }

    public CachedProductRepository (IProductRepository productRepository)
    {
        ProductRepository = productRepository;
    }

    public IList<Product> GetAll()
    {
        // Check cache, if exists then return, 
        // if not then call GetAll() on inner repository
    }
}

看看您如何从ProductManager中删除存储库实现知识?还通过拥有一个处理数据提取的类,一个处理数据检索的类和一个处理缓存的类来了解您如何遵守“单一职责原则”?

现在,您可以使用这些存储库中的任何一个实例化ProductManager并获取缓存...。稍后,当您怀疑是由于缓存导致的令人困惑的错误时,这将非常有用。

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

productManager = new ProductManager(
                         new CachedProductRepository(new SqlProductRepository())
                         );

(如果您使用的是IOC容器,那就更好了。如何进行适应应该很明显。)

并且,在您的ProductManager测试中

IProductRepository repo = MockRepository.GenerateStrictMock<IProductRepository>();

完全不需要测试缓存。

现在的问题变成:我应该测试CachedProductRepository吗?我不建议。缓存非常不确定。该框架会执行您无法控制的事情。例如,仅在装满时从其中取出东西。您将最终获得一次失败的测试,而该测试一次在蓝月亮中失败了,您将永远无法真正理解为什么。

而且,在做出了我上面建议的更改之后,实际上没有太多逻辑可以在其中进行测试。真正重要的测试是过滤方法,它将从GetAll()的细节中完全抽象出来。GetAll()只是...获取全部。从某处。


如果您在ProductManager中使用CachedProductRepository但想使用SQLProductRepository中的方法,该怎么办?
2014年

@Jonathan:“只要具有使用相同接口的存储库和缓存包装器”,如果它们具有相同的接口,则可以使用相同的方法。调用代码不需要了解任何有关实现的知识。
pdr

3

您建议的方法是我要做的。根据您的描述,无论对象是否存在于缓存中,该方法的结果都应该相同:您仍应获得相同的结果。通过在每次测试之前以特定方式设置缓存,可以轻松地进行测试。可能还有其他一些情况,例如guid是null或没有对象具有请求的属性;那些也可以测试。

此外,您可能认为在方法返回之后该对象应该存在于缓存中,而不管对象是否首先位于缓存中。这是有争议的,因为有些人(包括我自己在内)会争辩说,您关心的从接口返回的内容,而不是如何获得接口(即,测试接口是否按预期工作,而不是具有特定的实现)。如果您认为它很重要,则有机会进行测试。


1

我考虑过在TestInitialize上填充缓存,并在TestCleanup上删除缓存,但这对我来说不合适

实际上,这是唯一正确的方法。这就是这两个功能的用途:设置前提条件并清理。如果不满足前提条件,则您的程序可能无法运行。


0

我最近在进行一些使用缓存的测试。我围绕与缓存一起使用的类创建了一个包装器,然后断言该包装器已被调用。

我这样做主要是因为与缓存一起使用的现有类是静态的。


0

看起来您想测试缓存逻辑,而不是填充逻辑。因此,我建议您嘲笑不需要测试的内容-填充。

您的AllFromCache()方法负责填充缓存,应该将其委托给其他东西,例如值的提供者。所以你的代码看起来像

private Supplier<TObject> supplier;

IList<TObject> AllFromCache() {
    if (!cacheInitialized) {
        //whatever logic needed to fill the cache
        cache.putAll(supplier.getValues());
        cacheInitialized = true;
    }

    return  cache.getAll();
}

现在,您可以模拟测试的供应商,以返回一些预定义的值。这样,您可以测试实际的过滤和提取,而不加载对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.