В 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 довольно хорошо раскрыта, но необходимость 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));

Заключение


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