Написание тестов имеет решающее значение. Иногда это сложно, иногда скучно, но нужно доказать, что система работает правильно. Когда приложение включает в себя базу данных, очередь или другие зависимости, вам необходимо написать интеграционные тесты, подтверждающие, что интеграция с этими системами выполнена правильно. Вам необходимо проверить правильность использования API или правильность запросов SQL. Однако для этого вам необходимо, чтобы эти зависимости были доступны во время тестов. Как? Давай обсудим это сегодня.

До и после

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

  1. Время - часто требуется время, чтобы запустить встроенную базу данных. Чем чаще вы его запускаете и останавливаете, тем дольше длится тестирование. Если начинать и останавливаться на каждом тестовом примере, то вся фаза тестирования проекта может занять много времени. Вот почему принято начинать BeforeAll-подобными и останавливаться AfterAll-подобными методами. Однако это имеет некоторые последствия.
  2. Состояние - база данных имеет свое состояние, и если она запускается только один раз, перед всеми тестами в наборе тестов, то дальнейшие тесты должны иметь дело с грязным состоянием. Для этого есть несколько обходных путей, которые работают для баз данных, но не обязательно, например, для очередей:
  • чтобы удалить и воссоздать схему после каждого теста,
  • обрезать данные после каждого теста,
  • для отката транзакций,
  • чтобы сделать тесты независимыми, используя разные идентификаторы объектов, а для тестов таких методов, как getAll, проверять, как состояние изменилось во время теста, не утверждая все содержимое ответа.

Давайте посмотрим, как это может выглядеть на практике на Java с JUnit 4 (@Before и @After или @BeforeClass и @AfterClass)

или используя правила JUnit 4 (@Rule или @ClassRule):

или с JUnit 5 (@BeforeEach и @AfterEach или @BeforeAll и @AfterAll):

или с помощью Scala и Scalatest (BeforeAndAfterEach или BeforeAndAfterAll)

JUnit 5 также имеет эквивалент правил JUnit 4, основанный на аннотации @ExtendWith, однако в cassandra-unit для этого нет встроенного класса.

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

С Java у вас есть еще один выбор - использовать проект Testcontainers. Он позволяет легко интегрировать контейнеры Docker с выполнением ваших тестов. Например:

Данный фрагмент кода запускал бы целый контейнер Cassandra для каждого тестового примера. Конечно, вам не обязательно использовать только общие классы, потому что модули Testcontainers поддерживают довольно большое количество различных баз данных и серверов. Если вы хотите проводить сквозные типовые тесты, вы даже можете запустить Docker Compose .yml.

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

Наборы тестов

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

Недостатки? Вы не можете просто запустить один тест, потому что база данных не запустится. Если вы хотите это сделать, вам нужно запустить его отдельно. Это затрудняет локальную разработку / отладку.

Зависимости за пределами

Другой подход - просто запустить внешние зависимости перед запуском тестов. Вы можете запускать их изначально или в контейнерах. Обычно мы храним сценарии создания Docker вместе с нашими проектами. Они содержат все необходимое для работы службы. Когда новый человек присоединяется, ему нужно запустить только две вещи - упомянутую docker-compose и приложение с настройками по умолчанию. Для тестов можно использовать одни и те же сценарии, только с разными именами схем (чтобы тесты не очищали состояние, которое вы поддерживаете для работы локальной службы), очередей или тем. Таким образом, база данных запускается один раз перед всеми тестами, поэтому накладные расходы меньше. Однако это порождает другие проблемы. Вам нужно помнить о состоянии, но также вам нужно запустить его на CI перед тестами. Иногда докер недоступен на машине CI, а иногда приходится иметь дело с конфликтами портов между различными исполнениями, которые могут выполняться параллельно. Это определенно требует больших усилий от DevOps. К счастью, сегодня различные CI имеют встроенную поддержку докеров - например, CircleCI или Google Cloud Build.

Выводы

Тесты должны быть быстрыми, чтобы люди могли проводить их локально. Не существует идеального подхода к запуску внешних зависимостей ваших тестов. Вам нужно решить, что вам удобнее и будет ли ваша CI обрабатывать спроектированный поток. Помните, что в большинстве решений вы не ограничены только JUnit, но часто можете использовать Spock или другие тестовые среды также из других языков, например Scala.