При написании интеграционных тестов для реляционной базы данных зачастую полезно выполнять 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, чтобы выполнить 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:

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 и слушателя SqlScriptsTestExecutionListener содержат подробную информацию, а в следующем примере показан типичный сценарий тестирования с использованием 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 для ознакомления с примерами и получения более подробной информации.