При написанні інтеграційних тестів для реляційної бази даних найчастіше корисно виконувати 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
.
Більш детальну інформацію дивися в 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
та слухача SqlScriptsTestExecution
містять детальну інформацію, а в наступному
прикладі
показаний типовий сценарій тестування з використанням 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
для ознайомлення з прикладами та отримання більш детальної інформації.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ