При написанні інтеграційних тестів для реляційної бази даних найчастіше корисно виконувати 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:

Java

@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);
    // запускаємо код, який використовує тестову схему та дані
}
Kotlin

@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:

Java

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
    @Test
    void emptySchemaTest() {
         // виконуємо код, який використовує тестову схему без будь-яких тестових даних
    }
    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    void userTest() {
        // виконуємо код, який використовує тестову схему та тестові дані
    }
}
Kotlin

@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:

Java

@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql") void userTest() {
    // виконуємо код, який використовує тестову схему та тестові дані
}
Kotlin
// Повторювані анотації з не-SOURCE збереженням поки що не підтримуються Kotlin

У сценарії, показаному в попередньому прикладі, скрипт test-schema.sql використовує інший синтаксис для однорядкових коментарів.

Наступний приклад ідентичний попередньому, за винятком того, що оголошення анотації @Sql згруповані разом в анотації @SqlGroup. У версії Java 8 і вище використання анотації @SqlGroup необов'язкове, але анотацію @SqlGroup може знадобитися використовувати для сумісності з іншими мовами JVM, такими як Kotlin.

Java

@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql(" /test-user-data.sql")
)}
void userTest() {
    // виконуємо код, який використовує тестову схему та тестові дані
}
Kotlin

@Test
@SqlGroup(
    Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
    Sql("/test-user-data.sql"))
fun userTest() {
    // виконуємо код, який використовує тестову схему та тестові дані
}
Фази виконання скриптів

За замовчуванням SQL-скрипти виконуються перед відповідним тестовим методом. Однак, якщо потрібно запустити певний набір скриптів після тестового методу (наприклад, для очищення стану бази даних), можна використовувати атрибут executionPhase в анотації @Sql, як показано в наступному прикладі:

Java

@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() {
    // виконуємо код, який наказує фіксувати тестові дані
    // у базі даних поза тестовою транзакцією
}
Kotlin

@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:

Java

@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.");
    }
}
Kotlin

@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 для ознайомлення з прикладами та отримання більш детальної інформації.