Лучшие практики для методов модульного тестирования, которые интенсивно используют кэш?

17

У меня есть несколько методов бизнес-логики, которые хранят и извлекают (с фильтрацией) объекты и списки объектов из кэша.

Рассмотреть возможность

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

TObject FetchById(guid id) { ... }

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

Fetch..и Filter..вызвал бы, AllFromCacheкоторый заполнил бы кэш и возвратился бы, если это не там, и просто возвратился бы от этого, если это есть.

Я вообще избегаю модульного тестирования этих. Каковы лучшие практики для модульного тестирования против такого типа структуры?

Я рассмотрел заполнение кэша в TestInitialize и удаление в TestCleanup, но мне это не кажется правильным (хотя это вполне может быть).

NikolaiDante
источник

Ответы:

18

Если вам нужны настоящие юнит-тесты, то вам нужно смоделировать кеш: написать фиктивный объект, который реализует тот же интерфейс, что и кеш, но вместо того, чтобы быть кешем, он отслеживает полученные вызовы и всегда возвращает то, что реально кеш должен возвращаться в соответствии с тестовым примером.

Конечно, сам кэш также нуждается в модульном тестировании, для которого вам нужно высмеивать все, от чего он зависит, и так далее.

То, что вы описываете, используя реальный объект кеша, но инициализируя его в известное состояние и очищая после теста, больше похоже на интеграционный тест, потому что вы тестируете несколько модулей совместно.

tdammers
источник
+1 это демонстративно лучший подход. Модульный тест для проверки логики, а затем интеграционный тест для проверки того, что кэш работает так, как вы ожидаете.
Том Сквайрс
10

Принцип единой ответственности - ваш лучший друг здесь.

Прежде всего, переместите AllFromCache () в класс репозитория и назовите его GetAll (). То, что он извлекает из кэша, является подробностью реализации хранилища и не должно быть известно вызывающему коду.

Это делает тестирование вашего класса фильтрации приятным и легким. Больше не волнует, откуда ты это взял.

Во-вторых, оберните класс, который получает данные из базы данных (или где-либо еще) в оболочку кэширования.

АОП является хорошей техникой для этого. Это одна из немногих вещей, в которых она очень хороша.

Используя такие инструменты, как PostSharp , вы можете настроить его так, чтобы любой метод, отмеченный выбранным атрибутом, был кэширован. Однако, если это единственное, что вы кэшируете, вам не нужно заходить так далеко, чтобы иметь AOP-фреймворк. Просто имейте Repository и Caching Wrapper, которые используют один и тот же интерфейс, и вставьте его в вызывающий класс.

например.

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 () просто ... получает все. Откуда-то

прецизионный самописец
источник
Что делать, если вы используете CachedProductRepository в ProductManager, но хотите использовать методы, которые есть в SQLProductRepository?
Джонатан
@Jonathan: «Просто имейте Repository и Caching Wrapper, которые используют один и тот же интерфейс» - если они имеют одинаковый интерфейс, вы можете использовать те же методы. Вызывающему коду не нужно ничего знать о реализации.
фунтовые
3

Ваш предложенный подход - то, что я сделал бы. Учитывая ваше описание, результат метода должен быть одинаковым независимо от того, присутствует объект в кэше или нет: вы все равно должны получить тот же результат. Это легко проверить, настроив кеш определенным образом перед каждым тестом. Возможно, есть несколько дополнительных случаев, например, если guid имеет nullили не имеет объекта, у которого есть запрошенное свойство; те могут быть проверены тоже.

Кроме того, вы можете считать ожидаемым, что объект будет присутствовать в кеше после возврата вашего метода, независимо от того, был ли он в кеше вообще. Это спорно, так как некоторые люди (включая меня) , утверждают , что вы заботитесь о том, что вы получите обратно из своего интерфейса, а не как вы получите его (то есть ваше тестирование , что интерфейс работает , как и ожидалось, не то, что он имеет конкретной реализации). Если вы считаете это важным, у вас есть возможность проверить это.


источник
1

Я рассмотрел заполнение кэша в TestInitialize и удаление в TestCleanup, но мне это не подходит

На самом деле, это единственный правильный способ сделать. Вот для чего нужны эти две функции: установить предварительные условия и выполнить очистку. Если предварительные условия не выполнены, ваша программа может не работать.

BЈовић
источник
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();
}

Теперь вы можете издеваться над поставщиком для теста, чтобы вернуть некоторые предопределенные значения. Таким образом, вы можете проверить фактическую фильтрацию и выборку, а не загружать объекты.

jmruc
источник