Кращі практики для одиничних методів тестування, які активно використовують кеш?


17

У мене є ряд методів ділової логіки, які зберігають та отримують (з фільтруванням) об'єкти та списки об’єктів із кешу.

Розглянемо

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

TObject FetchById(guid id) { ... }

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

Fetch..і Filter..зателефонував би, AllFromCacheякий заповнить кеш і повернеться, якщо його немає, і просто повернеться з нього, якщо він є.

Я взагалі ухиляюся від тестування одиниць. Які найкращі практики для тестування одиниць на цій структурі?

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

Відповіді:


18

Якщо вам потрібні справжні тестові модулі, вам доведеться знущатися з кешу: написати об'єкт макету, який реалізує той самий інтерфейс, що і кеш, але замість того, щоб кеш, він відслідковує отримані дзвінки та завжди повертає те, що є справжнім кеш повинен повертатися відповідно до тестового випадку.

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

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


+1 це зухвало найкращий підхід. Тест модуля для перевірки логіки, а потім тест інтеграції, щоб перевірити, чи кеш працює, як ви очікували.
Tom Squires

10

Принцип єдиної відповідальності - ваш найкращий друг тут.

Перш за все, перемістіть AllFromCache () у клас сховища та назвіть його GetAll (). Те, що воно витягується з кеша, є деталлю реалізації репозиторію і його не повинно знати кодовий код.

Це робить тестування вашого класу фільтрації приємним та простим. Це вже не хвилює, звідки ви його отримуєте.

По-друге, загортайте клас, який отримує дані з бази даних (або деінде), в обгортку кешування.

AOP - хороша техніка для цього. Це одна з небагатьох речей, в якій це дуже добре.

Використовуючи такі інструменти, як PostSharp , ви можете налаштувати його так, що будь-який метод, позначений вибраним атрибутом, буде кешований. Однак, якщо це єдине, що ви кешуєте, вам не потрібно йти так далеко, щоб мати рамку 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 () просто ... отримує все. Звідкись.


Що робити, якщо ви використовуєте CachedProductRepository в ProductManager, але хочете використовувати методи, що знаходяться в SQLProductRepository?
Джонатан

@Jonathan: "Просто мати сховище та обробку кешування, які використовують один і той же інтерфейс" - якщо вони мають один інтерфейс, ви можете використовувати ті самі методи. Коду виклику не потрібно нічого знати про реалізацію.
пдр

3

Ваш запропонований підхід - це те, що я б зробив. З огляду на ваш опис, результат методу повинен бути однаковим, присутній об’єкт у кеші чи ні: ви все одно отримаєте той самий результат. Це легко перевірити, налаштовуючи кеш певним чином перед кожним тестом. Ймовірно, є деякі додаткові випадки, наприклад, якщо вказівник є 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();
}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.