Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В 2018-м году, работая в Akvelon Inc., я собеседовал одного человека. Перед интервью мне дали на проверку его тестовое задание: небольшое web-приложение по типу записной книжки или todo-списка – React\TypeScript, C# на бэке и MS SQL Server в качестве персистентного хранилища. Приложение было модное: с обилием unit-тестов на mock’ах, упакованное в docker-образ – видно, что человек старался. И у этого решения был всего один недостаток – оно не работало. Совсем. Падало при попытке сохранить новую строку в базу данных.
Этот случай мне очень хорошо запомнился, поскольку подсветил сразу несколько типовых проблем.
Первая из них – ложная уверенность от модульных тестов. Даже 100% покрытие кода тестами не гарантирует, что в нём нет ошибок.
И вторая – отсутствие функциональных тестов. Если ваше приложение работает с СУБД, то вы обязательно должны покрыть эту часть кода реальными тестами с реальной базой данных. И здесь есть очень важное условие: проверять нужно именно на той версии СУБД, которая работает у вас в production’е. Думаю, очень многие разработчики под Oracle, прогоняющие свои тесты на H2\HSQLDB, сталкивались с ситуацией, когда тесты проходят, а production не работает (boolean, group by и другие чудеса).
Сейчас я работаю в основном с PostgreSQL и мигрирую наши микросервисы с 10-й версии на 11-ую. В процессе миграции (и разработки вообще) я столкнулся с несколькими нюансами, о которых хотелось бы рассказать.
Используйте современные инструменты
Первое, с чего начну: прекратите использовать Embedded PostgreSQL из Yandex QATools. Проект устарел и давно не развивается. В качестве современных альтернатив стоит рассмотреть:
- либо TestContainers,
- либо otj-pg-embedded,
- либо его форк от Zonky.
На Хабре тема использования TestContainers довольно хорошо раскрыта, но необходимость Docker’а сделала проект малопригодным для использования в нашей инфраструктуре.
В итоге мы долгое время использовали именно otj-pg-embedded. О том, как интегрировать его с вашим проектом, можно почитать в статье ребят из HeadHunter или на DZone. Главное отличие от аналога из Yandex QATools в том, что otj-pg-embedded нормально работает под Windows и MacOS и предоставляет вменяемые сообщения об ошибках, если что-то вдруг пойдёт не так при инициализации тестовой БД. А ещё есть поддержка Liquibase и Flyway «из коробки»:
abstract class DatabaseAwareTestBase {
@RegisterExtension
static final PreparedDbExtension embeddedPostgres =
EmbeddedPostgresExtension.preparedDatabase(
LiquibasePreparer.forClasspathLocation("changelogs/changelog.xml"));
}
Небольшой демонстрационный проект можно найти у меня на GitHub.
Правильно очищайте таблицы БД после тестирования
Второй момент связан с очисткой БД между тестами. Типовой сценарий использования PostgreSQL в функциональных тестах довольно прост: поднимаем экземпляр БД перед тестами, накатываем миграции и запускаем все тесты на этом экземпляре.
Конечно, очень хорошо, когда каждый тест восстанавливает БД в первоначальное состояние по окончанию своей работы, но, увы, на практике это не всегда достижимо. Есть вариант с запуском, каждого теста внутри транзакции и её откате со всеми изменениями по окончанию теста.
Альтернативой ему является очистка всех целевых таблиц по окончанию каждого теста. Разумеется, вариант с delete from <table>
никуда не годится в плане скорости работы. Единственный приемлемый по скорости способ очистки заключается в использовании команды truncate. Нюанс в том, как указать таблицы в скрипте очистки. Можно последовательно вызывать truncate для всех таблиц, добавляя где нужно инструкцию cascade:
truncate table tableA;
truncate table tableB cascade;
truncate table tableE;
Но можно сделать гораздо лучше, вспомнив одно простое правило: чем меньше обращений к БД, тем лучше:
truncate table tableA, tableB, tableC, tableD, tableE;
В этом случае не нужна инструкция cascade, достаточно указать все связанные таблицы в команде; об остальном позаботится СУБД. Если у вас много таблиц, то прирост скорости очистки может вас приятно удивить.
Обязательно проверяйте версию СУБД в тестах
Третья вещь, которую вам следует внедрить в ваших тестах, это проверка версии СУБД, на которой они запускаются. Как я уже сказал, сейчас мы мигрируем наши базы на 11-й Postgres (причем 11-ая версия промежуточная, дальше будем переходить на 12-ую). Для этого нам пришлось отказаться от otj-pg-embedded в пользу его форка от компании Zonky, поскольку он позволяет более удобным и простым способом изменить версию СУБД, указав её в зависимостях:
testImplementation enforcedPlatform('io.zonky.test.postgres:embedded-postgres-binaries-bom:11.6.0')
testImplementation 'io.zonky.test:embedded-postgres:1.2.6'
Сам тест максимально простой, но позволит вам на 100% гарантировать, что остальные тесты запускаются на правильной версии СУБД.
@Test
void checkPostgresVersion() {
final String pgVersion = jdbcTemplate.queryForObject("select version();", String.class);
assertThat(pgVersion, startsWith("PostgreSQL 11.6"));
}
Запускайте тесты на всех платформах
Четвёртый совет немного капитанский, но не менее важный – запускайте тесты на всех платформах. Я много раз сталкивался с Java-проектами, которые работают только на Linux, и этому нет оправдания. Кроме того, во всех программных продуктах бывают ошибки (например, такие), и ваш CI-пайплайн может отловить их раньше, чем с ними столкнутся ваши разработчики.
Используйте SQL для описания миграций БД
Пятое: используйте plain SQL для описания миграций вашей БД. Забудьте про xml, yml и прочее, будьте проще и ближе к СУБД, общайтесь с базой на одном языке. Иногда бывают ситуации, когда нужно проверить какую-нибудь гипотезу\миграцию на локальной БД. Вытащить скрипт создания таблицы из xml-файла и выполнить его в psql\pgAdmin – не самая тривиальная задача. C plain SQL миграциями вы сэкономите себе немало времени. Сравните
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<changeSet id="orders_table_create_2020-05-09" author="ivan.vakhrushev">
<createTable tableName="orders">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="shop_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="buyer_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)">
<column name="creation_time" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="update_time" type="TIMESTAMP"/>
</createTable>
</changeSet>
</databaseChangeLog>
и
--liquibase formatted sql
--changeset ivan.vakhrushev:orders_table_create_2020-05-09
create table if not exists orders
(
id bigint not null primary key,
shop_id bigint not null,
buyer_id bigint not null,
status varchar(20),
creation_time timestamp not null,
update_time timestamp
);
Следите за изменениями в языке программирования и СУБД
И напоследок. При переходе с Java 8 на Java 11 часть наших тестов стала случайным образом падать при локальном запуске. Проблема была вызвана изменением точности Instant\LocalDateTime. PostgreSQL хранит Timestamp с точностью до микросекунд, просто отсекая «лишние» знаки после запятой. В Java мы имеем точность до наносекунд. В итоге от этого страдали те тесты, которые проверяли наличие в БД «актуальной» на данный момент записи сразу после её вставки. Как вариант быстрого лечения, перед записью в БД можно сделать что-то типа:
Timestamp.valueOf(localDateTime.truncatedTo(ChronoUnit.MICROS));
Timestamp.from(instant.truncatedTo(ChronoUnit.MICROS));
Заключение
На сегодня всё. Смею надеяться, что какой-нибудь из этих советов окажется для вас полезным.
Чистого вам кода и меньше багов.