Статьи

Stubbing Key-Value Stores

Каждый проект, имеющий базу данных, имеет дилемму: как тестировать код, зависящий от базы данных Есть несколько вариантов (не взаимоисключающих):

  • Используйте mocks — используйте только модульные тесты и смоделируйте уровень доступа к данным, предполагая, что связь между DAO и базой данных работает
  • Используйте встроенную базу данных, чтобы каждый тест запускался и выключался. Это также можно рассматривать как юнит-тестирование
  • Используйте настоящую базу данных, развернутую где-то (локально или в тестовой среде). Самое сложное — убедиться, что он всегда в чистом состоянии.
  • Используйте сквозные / функциональные тесты / bdd / UI тесты после развертывания приложения на тестовом сервере (который имеет соответствующую базу данных).

Ничто из вышеперечисленного не обходится без проблем. Модульные тесты с поддельными DAO не могут реально протестировать более сложные взаимодействия, основанные на состоянии базы данных. Встроенные базы данных не всегда доступны (например, если вы используете нереляционную базу данных, или если вы полагаетесь на функциональность, специфичную для СУБД, HSQLDB не подойдет), или они могут запускаться медленно, и ваши тесты могут занять слишком много времени поддержка. Реальная установка базы данных усложняет настройку, и поддерживать ее в чистоте не всегда просто. Охват сквозных тестов не может быть легко измерен, и они не обязательно охватывают все крайние случаи, поскольку их сложнее поддерживать, чем модульные и интеграционные тесты.

Я недавно попробовал странный подход, который до сих пор работает довольно хорошо — заглушка базы данных. Это применимо больше к хранилищам ключ-значение и меньше к реляционным базам данных.

В моем случае, несмотря на наличие встроенной кассандры , запуск происходил медленно, его было нелегко настроить, и у него были небольшие проблемы. Вот почему я заменил все это в ConcurrentHashMap в памяти.

Поскольку я использую spring-data-cassandra, я просто расширил класс CassandraTemplate и реализовал все методы в новом StubCassandraTemplate и использовал его вместо обычного в контексте тестовой пружины. Заглушка может довольно легко поддерживать все операции ключ / значение, и у вас могут быть немного более сложные интеграционные тесты (конечно, не очень хорошо иметь очень сложные тесты, но модульные тесты могут быть либо слишком простыми, либо слишком зависимыми от много издевательств). Вот выдержка из кода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component("cassandraTemplate")
public class StubCassandraTemplate extends CassandraTemplate {
     
    private Map<Class<?>, Map<Object, Object>> data = new ConcurrentHashMap<>();
     
    @Override
    public void afterPropertiesSet() {
        // no validation
    }
     
    @SuppressWarnings("unchecked")
    @Override
    public <T> T insert(T entity) {
        List<Field> pk = FieldUtils.getFieldsListWithAnnotation(entity.getClass(), PrimaryKey.class);
        initializeClass(entity.getClass());
        try {
            pk.get(0).setAccessible(true);
            return (T) data.get(entity.getClass()).put(pk.get(0).get(entity), entity);
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException(e);
        }
    }
 
    private <T> void initializeClass(Class<?> clazz) {
        if (data.get(clazz) == null) {
            data.put(clazz, new ConcurrentHashMap<>());
        }
    }
....
}

Cassandra поддерживает некоторые расширенные функции, такие как CQL (язык запросов), который не так просто заглушить, как операции со значением ключа, такие как get и put, но на самом деле это не так сложно. Особенно, если вы не полагаетесь на сложные операторы where (а это в любом случае плохая практика в Cassandra), легко проанализировать запрос с помощью регулярного выражения и найти соответствующие записи в ConcurrentHashMap .

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

Обратите внимание, что эти тесты не гарантируют, что приложение будет работать с реальной базой данных. Они только гарантируют, что он будет вести себя правильно, если база данных будет вести себя так же, как структура данных значения ключа в памяти. Как правило, это предположение, но не всегда верно — например, база данных может накладывать дополнительные ограничения, которых нет в вашей реализации заглушки. Кассандра, например, не разрешает запросы WHERE для неиндексированных столбцов. Если вы не примете это во внимание, очевидно, ваш тест пройдет, но ваше приложение будет сломано.

Вот почему вам по-прежнему нужны сквозные тесты и, возможно, некоторые реальные интеграционные тесты, но вы можете покрыть большую часть кода с помощью простой заглушки в памяти и выполнить только некоторые «полноценные» полные интеграционные тесты.