Интеграционное тестирование и Spring JDBC

testing-integration-cПочти все примеры в статьях и о JDBC и о Spring JDBC были написаны по одному шаблону — подготавливаем структуру базы данных, наполняем её тестовыми данными, исполняем какой-то код на тестовых данных, очищаем базу данных. В статье о признаках хорошего теста я писал, что так устроен практически любой тест: подготовка тестовой среды, выполнение теста, очистка тестовой среды.

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

С другой стороны, Spring JDBC предоставляет утилиты, упрощающие разработку интеграционных тестов для кода, работающего с базами данных.

Подготовка

За основу я взял код из примера «Запросы в Spring JDBC» и немного его изменил. В первую очередь, встроенная база H2 заменена на :

Кроме того, я подготовил пустую базу в PostgreSQL сервере:

SQL скрипты, которые создают структуру базы и наполняют её данными, я разбил на отдельные скрипты создания каждой таблицы и отдельные скрипты для наполнения этих таблиц данными.

Выполнение SQL скриптов.

Класс ResourceDatabasePopulator позволяет выполнять SQL скрипты в указанном порядке. Звучит просто, но реально это очень мощный механизм для работы с данными. Его можно применять как в тестах, так и в каких-либо пакетных операциях над базой и вообще по любому поводу.

Я с его помощью буду инициализировать таблицы для интеграционного теста:

ResourceDatabasePopulator принимает SQL скрипты как экземпляры класса Resource, который скрывает истинный источник данных и позволяет использовать файлы из classpath, файлы из файловой системы, напрямую из интернета, байтовые потоки и т.д. Скрипты выполняются в порядке выполнения. Кроме задания списка скриптов можно задать условия их выполнения: разделитель запросов, символы комментирования, игнорирование ошибок и прочая. Готовый, сконфигурированный DatabasePopulator можно исполнить на базе данных с помощью статического метода execute()  класса DatabasePopulatorUtils. Метод execute() ожидает получить объект DataSource, а не JdbcTemplate в качестве ссылки на базу данных.

JdbcTestUtils

Второй вспомогательный класс имеет название, намекающее, что он ориентирован только на тесты. В основном он ориентирован на очищение базы данных после теста.

В данном случае он удаляет таблицу customers, создаваемую скриптами в методе setUp(). Размещение инициализации/деинициализации в методах @Before/@After гарантирует, что каждый тест класса получит одинаковый набор данных, заданный скриптами.

Автоматическое выполнение скриптов

Если внимательно присмотреться к коду setUp() выше, видно, что вообщем-то для разных тестов он будут отличаться только списком скриптов, а в остальном будет тот же самый. Spring JDBC позволяет не повторяться и не копипастить ResourceDatabasePopulator из теста в тест, а указывать набор скриптов в качестве метаданных теста или тестового класса.

Аннотации @SqlGroup и @Sql говорят Spring test framework, что перед запуском каждого теста (поведение по умолчанию) или после завершения каждого теста (если указан параметр executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) необходимо выполнить скрипт/группу скриптов. В примере выше перед запуском каждого теста выполняются два скрипта, которые создают таблицу и наполняют её данными. А после выполнения тестов запускается sql скрипт, удаляющий таблицу.

И опять JdbcTestUtils

В примере выше в тестовом методе используются два новых метода JdbcTestUtils. Вначале метод JdbcTestUtils.deleteFromTables() очищает таблицы, не удаляя их. Затем, после того как операции над базой выполнены, метод JdbcTestUtils.countRowsInTable() подсчитывает текущее количество строк в таблице. Результат подсчёта сравнивается с эталоном.

Оба метода имеют компаньонов, JdbcTestUtils.deleteFromTableWhere()  и JdbcTestUtils.countRowsInTableWhere(), которые позволяют указать условия для выполнения операции. Кроме того, метод JdbcTestUtils.deleteFromTables()  принимает несколько аргументов, позволяя очистить несколько таблиц сразу. Таким же поведением обладает и JdbcTestUtils.dropTables() из примера с @Before/@After, который так же способен удалить несколько таблиц.

Всё вместе.

Всё перечисленное выше можно комбинировать. Например таблицы можно создавать скриптом, а удалять в @After  методе.

Код примера доступен на github.