При написании интеграционных тестов для реляционной базы данных зачастую полезно выполнять SQL-скрипты для изменения схемы базы данных или вставки тестовых данных в таблицы. Модуль spring-jdbc
обеспечивает поддержку инициализации встроенной или существующей базы данных путем выполнения SQL-скриптов при загрузке ApplicationContext
в Spring. Подробнее см. в разделах "Поддержка встроенных баз данных" и "Тестирование логики доступа к данным с использованием встроенной базы данных".
Хотя и очень разумно инициализировать базу данных для тестирования единожды при загрузке ApplicationContext
, иногда всё же необходимо иметь возможность изменять базу данных во время интеграционных тестов. В следующих разделах объясняется, как запускать SQL-скрипты программно и декларативно во время интеграционных тестов.
Программное выполнение SQL-скриптов
Spring предоставляет следующие средства для выполнения SQL-скриптов программно в методах интеграционного тестирования.
-
org.springframework.jdbc.datasource.init.ScriptUtils
-
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
-
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
-
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils
предоставляет коллекцию статических методов для работы с SQL-скриптами и в основном предназначен для внутреннего использования в рамках фреймворка. Однако если необходим полный контроль над анализом и выполнением SQL-скриптов, ScriptUtils
может подходить лучше, чем некоторые иные альтернативы, описанные далее. Более подробную информацию смотрите в javadoc по отдельным методам в ScriptUtils
.
ResourceDatabasePopulator
предоставляет объектно-ориентированный API-интерфейс для программного заполнения, инициализации или очистки базы данных с помощью SQL-скриптов, определенных во внешних ресурсах. ResourceDatabasePopulator
предоставляет опции для конфигурирования кодировки символов, разделителя инструкций, разграничителей комментариев и флагов обработки ошибок, используемых при синтаксическом анализе и выполнении скриптов. Каждый из параметров конфигурации имеет значение по умолчанию. Подробности о значениях по умолчанию см. в javadoc. Для выполнения скриптов, сконфигурированных в ResourceDatabasePopulator
, можно вызвать либо метод populate(Connection)
, чтобы выполнить модуль заполнения в отношении java.sql.Connection
, либо метод execute(DataSource)
, чтобы выполнить модуль заполнения в отношении javax.sql.DataSource
. В следующем примере заданы SQL-скрипты для тестовой схемы и тестовых данных, разделитель инструкций установлен в значение @@
, а скрипты запускаются в отношении DataSource
:
@Test
void databaseTest() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// запускаем код, использующий тестовую схему и данные
}
@Test
fun databaseTest() {
val populator = ResourceDatabasePopulator()
populator.addScripts(
ClassPathResource("test-schema.sql"),
ClassPathResource("test-data.sql"))
populator.setSeparator("@@")
populator.execute(dataSource)
// запускаем код, использующий тестовую схему и данные
}
Обратите внимание, что ResourceDatabasePopulator
внутренне делегирует ScriptUtils
полномочия для синтаксического анализа и выполнения SQL-скриптов. Точно так же методы executeSqlScript(..)
в AbstractTransactionalJUnit4SpringContextTests
и AbstractTransactionalTestNGSpringContextTests
внутренне используют ResourceDatabasePopulator
, чтобы выполнить SQL-скрипты. Более подробную информацию смотрите в Javadoc по различным методам executeSqlScript(..)
.
Декларативное выполнение SQL-скриптов с помощью аннотации @Sql
В дополнение к вышеупомянутым механизмам для выполнения SQL-скриптов программно, можно декларативно конфигурировать SQL-скрипты в Spring TestContext Framework. В частности, можно объявить аннотацию @Sql
для тестового класса или тестового метода, чтобы сконфигурировать отдельные SQL-инструкции или пути к ресурсам SQL-скриптов, которые должны быть выполнены в отношении заданной базы данных перед или после метода интеграционного тестирования. Поддержка аннотации @Sql
обеспечивается слушателем SqlScriptsTestExecutionListener
, который активирован по умолчанию.
@Sql
на уровне метода по умолчанию переопределяют объявления на уровне классов. Однако, начиная со Spring Framework 5.2, эту логику работы можно сконфигурировать для каждого тестового класса или каждого тестового метода через аннотацию @SqlMergeMode
.Семантика ресурса пути
Каждый путь интерпретируется как Resource
из Spring. Простой путь (например, "schema.sql"
) считается ресурсом пути классов, который относиться к пакету, в котором определен тестовый класс. Путь, начинающийся с косой черты, считается абсолютным ресурсом пути классов (например, "/org/example/schema.sql"
). Путь, ссылающийся на URL-адрес (например, путь с префиксами classpath:
, file:
, http:
), загружается с использованием указанного протокола ресурса.
В следующем примере показано, как использовать аннотацию @Sql
на уровне класса и на уровне метода в классе интеграционного теста на основе JUnit Jupiter:
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// выполняем код, использующий тестовую схему без каких-либо тестовых данных
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// выполняем код, использующий тестовую схему и тестовые данные
}
}
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
fun emptySchemaTest() {
// выполняем код, использующий тестовую схему без каких-либо тестовых данных
}
@Test
@Sql("/test-schema.sql", "/test-user-data.sql")
fun userTest() {
// выполняем код, использующий тестовую схему и тестовые данные
}
}
Обнаружение скриптов по умолчанию
Если скрипты или инструкции SQL не заданы, предпринимается попытка определить default
скрипт в зависимости от того, где объявлена аннотация @Sql
. Если значение по умолчанию не может быть определено, будет сгенерировано исключение IllegalStateException
.
-
Объявление на уровне класса: Если аннотированный тестовый класс -
com.example.MyTest
, то соответствующий скрипт по умолчанию -classpath:com/example/MyTest.sql
. -
Объявление на уровне метода: Если аннотированный тестовый метод имеет имя
testMethod()
и определен в классеcom.example.MyTest
, то соответствующий скрипт по умолчанию будет называтьсяclasspath:com/example/MyTest.testMethod.sql
.
Объявление нескольких наборов аннотаций @Sql
Если нужно настроить несколько наборов SQL-скриптов для данного класса или метода тестирования, но с разной конфигурацией синтаксиса, разными правилами обработки ошибок или разными фазами выполнения для каждого набора, можно объявить несколько экземпляров аннотации @Sql
. В Java 8 можно использовать аннотацию @Sql
в качестве повторяющейся аннотации. Или же можно использовать аннотацию @SqlGroup
как явный контейнер для объявления нескольких экземпляров аннотации @Sql
.
В следующем примере показано, как использовать аннотацию @Sql
в качестве повторяющейся аннотации в Java 8:
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// выполняем код, использующий тестовую схему и тестовые данные
}
// Повторяющиеся аннотации с не-SOURCE сохранением пока еще не поддерживаются Kotlin
В сценарии, представленном в предыдущем примере, скрипт test-schema.sql
использует иной синтаксис для однострочных комментариев.
Следующий пример идентичен предыдущему, за исключением того, что объявления аннотации @Sql
сгруппированы вместе в аннотации @SqlGroup
. В версии Java 8 и выше использование аннотации @SqlGroup
необязательно, но аннотацию @SqlGroup
может понадобиться использовать для совместимости с другими языками JVM, такими как Kotlin.
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// выполняем код, использующий тестовую схему и тестовые данные
}
@Test
@SqlGroup(
Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
Sql("/test-user-data.sql"))
fun userTest() {
// выполняем код, использующий тестовую схему и тестовые данные
}
Фазы выполнения скриптов
По умолчанию SQL-скрипты выполняются перед соответствующим тестовым методом. Однако если нужно запустить определенный набор скриптов после тестового метода (например, для очистки состояния базы данных), можно использовать атрибут executionPhase
в аннотации @Sql
, как показано в следующем примере:
@Test
@Sql(
scripts = "create-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
void userTest() {
// выполняем код, который предписывает фиксировать тестовые данные
// в базе данных вне тестовой транзакции
}
@Test
@SqlGroup(
Sql("create-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED)),
Sql("delete-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD))
fun userTest() {
// выполняем код, который предписывает фиксировать тестовые данные
// в базе данных вне тестовой транзакции
}
Обратите внимание, что ISOLATED
и AFTER_TEST_METHOD
статически импортированы из Sql.TransactionMode
и Sql.ExecutionPhase
, соответственно.
Конфигурирование скрипта с помощью @SqlConfig
Можно сконфигурировать синтаксический анализ скрипта и обработку ошибок с помощью аннотации @SqlConfig
. Если @SqlConfig
объявляется как аннотация на уровне класса для класса интеграционного теста, то она служит глобальной конфигурацией для всех SQL-скриптов в иерархии тестового класса. При прямом объявлении с помощью атрибута config
аннотации @Sql
, аннотация @SqlConfig
служит локальной конфигурацией для SQL-скриптов, объявленных в аннотации @Sql
. Каждый атрибут в аннотации @SqlConfig
имеет неявное значение по умолчанию, которое документировано в javadoc соответствующего атрибута. Из-за правил, определенных для атрибутов аннотаций в спецификации языка Java ( Java Language Specification), к сожалению, невозможно присвоить атрибуту аннотации значение null
. Таким образом, для поддержки переопределения унаследованной глобальной конфигурации атрибуты аннотации @SqlConfig
имеют явное значение по умолчанию либо ""
(для строк), {}
(для массивов), либо DEFAULT
(для перечислений). Этот подход позволяет локальным объявлениям аннотации @SqlConfig
выборочно переопределять отдельные атрибуты из глобальных объявлений аннотации @SqlConfig
, предоставляя значение, отличное от ""
, {}
или DEFAULT
. Глобальные атрибуты аннотации @SqlConfig
наследуются всегда, если локальные атрибуты аннотации @SqlConfig
не предоставляют явного значения, отличного от ""
, {}
или DEFAULT
. Поэтому явная локальная конфигурация имеет приоритет над глобальной конфигурацией.
Параметры конфигурации, предоставляемые аннотациями @Sql
и @SqlConfig
, эквивалентны тем, которые поддерживаются ScriptUtils
и ResourceDatabasePopulator
, но являются надмножеством тех, которые предоставляются элементом пространства имен XML <jdbc:initialize-database/>
. Подробности смотрите в javadoc по отдельным атрибутам в аннотациях @Sql
и @SqlConfig
.
Управление транзакциями для аннотации @Sql
По умолчанию SqlScriptsTestExecutionListener
создает желаемую семантику транзакций для скриптов, сконфигурированных с помощью аннотации @Sql
. В частности, SQL-скрипты выполняются без транзакции в рамках существующей транзакции, управляемой Spring (например, транзакции, управляемой TransactionalTestExecutionListener
для теста, помеченного аннотацией @Transactional
) или в рамках изолированной транзакции, в зависимости от сконфигурированного значения атрибута transactionMode
в аннотации @SqlConfig
и наличия PlatformTransactionManager
в ApplicationContext
теста. Однако, как минимум, javax.sql.DataSource
должен присутствовать в ApplicationContext
теста.
Если алгоритмы, используемые SqlScriptsTestExecutionListener
для определения DataSource
и PlatformTransactionManager
и вывода семантики транзакции не удовлетворяют вашим потребностям, то можно указать явные имена, установив атрибуты dataSource
и transactionManager
в аннотации @SqlConfig
. Кроме того, можно управлять логикой распространения транзакций, задавая атрибут transactionMode
в аннотации @SqlConfig
(например, следует ли запускать скрипты в изолированной транзакции). Хотя подробное рассмотрение всех поддерживаемых вариантов управления транзакциями с помощью аннотации @Sql
выходит за рамки данного справочного руководства, javadoc для аннотации @SqlConfig
и слушателя SqlScriptsTestExecutionListener
содержат подробную информацию, а в следующем примере показан типичный сценарий тестирования с использованием JUnit Jupiter и транзакционных тестов с аннотацией @Sql
:
@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {
final JdbcTemplate jdbcTemplate;
@Autowired
TransactionalSqlScriptsTests(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
@Sql("/test-data.sql")
void usersTest() {
// проверяем состояние в тестовой базе данных:
assertNumUsers(2);
// выполняем код, использующий тестовые данные...
}
int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
void assertNumUsers(int expected) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.");
}
}
@SpringJUnitConfig(TestDatabaseConfig::class)
@Transactional
class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) {
val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)
@Test
@Sql("/test-data.sql")
fun usersTest() {
// проверяем состояние в тестовой базе данных:
assertNumUsers(2)
// выполняем код, использующий тестовые данные...
}
fun countRowsInTable(tableName: String): Int {
return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
}
fun assertNumUsers(expected: Int) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.")
}
}
Обратите внимание, что нет необходимости очищать базу данных после выполнения метода usersTest()
, поскольку любые изменения, внесенные в базу данных (как в тестовом методе, так и в скрипте /test-data.sql
), автоматически откатываются TransactionalTestExecutionListener
(см. подробнее в разделе, посвященном управлениию транзакциями ).
Объединение и переопределение конфигурации с помощью аннотации @SqlMergeMode
Начиная со Spring Framework 5.2, можно объединять объявления аннотации @Sql
на уровне методов с объявлениями на уровне классов. Например, это позволит предоставлять конфигурацию для схемы базы данных или некоторые общие тестовые данные для тестового класса единожды, а затем предоставлять дополнительные, специфические для конкретного случая тестовые данные для каждого тестового метода. Чтобы активировать объединение аннотаций @Sql
, аннотируйте ваш тестовый класс или тестовый метод с помощью аннотации @SqlMergeMode(MERGE)
. Чтобы дезактивировать объединение для конкретного тестового метода (или конкретного тестового подкласса), можно переключиться обратно в режим по умолчанию с помощью аннотации @SqlMergeMode(OVERRIDE)
. Обратитесь к разделу документации, посвященному аннотации @SqlMergeMode
для ознакомления с примерами и получения более подробной информации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ