Шесть советов об использовании PostgreSQL в функциональных тестах

В 2018-м году, работая в Akvelon Inc., я собеседовал одного человека. Перед интервью мне дали на проверку его тестовое задание: небольшое web-приложение по типу записной книжки или todo-списка — React\TypeScript, C# на бэке и MS SQL Server в качестве персистентного хранилища. Приложение было модное: с обилием unit-тестов на mock«ах, упакованное в docker-образ — видно, что человек старался. И у этого решения был всего один недостаток — оно не работало. Совсем. Падало при попытке сохранить новую строку в базу данных.

dmimsbwf3txk0dgcfmtsvyjrmmu.jpeg

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

Первая из них — ложная уверенность от модульных тестов. Даже 100% покрытие кода тестами не гарантирует, что в нём нет ошибок.

И вторая — отсутствие функциональных тестов. Если ваше приложение работает с СУБД, то вы обязательно должны покрыть эту часть кода реальными тестами с реальной базой данных. И здесь есть очень важное условие: проверять нужно именно на той версии СУБД, которая работает у вас в production«е. Думаю, очень многие разработчики под Oracle, прогоняющие свои тесты на H2\HSQLDB, сталкивались с ситуацией, когда тесты проходят, а production не работает (boolean, group by и другие чудеса).

Сейчас я работаю в основном с PostgreSQL и мигрирую наши микросервисы с 10-й версии на 11-ую. В процессе миграции (и разработки вообще) я столкнулся с несколькими нюансами, о которых хотелось бы рассказать.


Используйте современные инструменты

Первое, с чего начну: прекратите использовать Embedded PostgreSQL из Yandex QATools. Проект устарел и давно не развивается. В качестве современных альтернатив стоит рассмотреть:

На Хабре тема использования 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

никуда не годится в плане скорости работы. Единственный приемлемый по скорости способ очистки заключается в использовании команды 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 миграциями вы сэкономите себе немало времени. Сравните


    
        
            
                
            
            
                
            
            
                
            
            
            
                
            
            
        
    

и

--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));


Заключение

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

© Habrahabr.ru