Простой и декларативный способ выполнять sql запросы в JUnit тестах.
Введение
Структура JUnit теста следует модели тестового сценария (test case):
ПредУсловия (PreConditions) - это действия, которые переводят тестируемую систему в определённое состояние необходимое для выполнения тестового сценария.
Тестовый сценарий (Test case) - это действия, которые меняют состояние тестируемой системы с целью сверить действительное поведение системы с ожидаемым.
ПостУсловия (PostConditions) - это действия, которые переводят тестируемую систему в первоначальное состояние, которое было до выполнения ПредУсловий.
JUnit предоставляет соответствующие аннотации согласно модели тестового сценария:
ПредУсловия (PreConditions) =
@BeforeEach
Тестовый сценарий (Test case) =
@Test
ПостУсловия (PostConditions) =
@AfterEach
Пример структуры в Java коде:
public class SomeTest {
@BeforeEach // PreConditions
void setUp() { ... }
@Test // Test Case
void testCase() { ... }
@AfterEach // PostConditions
void tearDown() { ... }
}
Представьте, что вам необходимо протестировать back-end приложение, которое подключается к системе управления базой данных (СУБД), например Postgresql. И вам необходимо вставить некоторые данные в СУБД до того как выполнить метод testCase()
:
public class SomeTest {
@BeforeEach // PreConditions
void setUp() {
String sql = "insert into department(id, name) values(1, 'dep1');";
// some code to execute sql query to database
}
@Test // Test Case
void testCase() { ... }
@AfterEach // PostConditions
void tearDown() {
String sql = "delete from department where id = 1;";
// some code to execute sql query to database
}
}
В данном случае, разработчику, помимо кодирования самого теста, необходимо написать реализацию выполнения запроса в СУБД. И эта реализация должна быть переиспользуемая, т.к. выполнять запросы нужно в двух методах, помеченные аннотациями @BeforeEach
и @AfterEach
.
Такой подход имеет следующие недостатки:
требует дополнительных временных затрат на написание реализации выполнения SQL запросов в СУБД
требует протестировать новое решение по выполнению SQL запросов в СУБД
сложно переиспользовать решение в других проектах
Также существует ещё один недостаток, которые значительно усложняет предложенную выше реализацию. Давайте посмотрим на него...
В чём проблема?
Давайте добавим новый тест:
public class SomeTest {
@BeforeEach // PreConditions
void setUp() {
String sql = "insert into department(id, name) values(1, 'dep1');";
// some code to execute sql query to database
}
@Test // Test Case
void testCase() { ... }
@AfterEach // PostConditions
void tearDown() {
String sql = "delete from department where id = 1;";
// some code to execute sql query to database
}
@BeforeEach // PreConditions
void setUp2() { ... }
@Test // Test Case
void testCase2() { ... }
@AfterEach // PostConditions
void tearDown2() { ... }
}
Методы setUp()
и setUp2()
будут выполнены для обоих тестов testCase()
и testCase2()
.
Почему?
Таков дизайн JUnit framework. В аннотациях @BeforeEach
не предоставляется информация к какому тестовому методу он относится. Поэтому JUnit выполняет его для всех тестовых методов определённых в классе.
Примечание
Есть возможность использовать объект TestInfo. В этом случае внутри метода setUp() и setUp2() можно добавить if и выполнять код с SQL запросами в зависимости от названия метода.
Спасибо за уточнение @MaxDM1993
Как решить эту проблему?
JUnit "из коробки" предлагает только одну возможность: оградить каждый тестовый сценарий вложенным классом:
public class SomeTest {
public static class TestCaseTest {
@BeforeEach // PreConditions
void setUp() {
String sql = "insert into department(id, name) values(1, 'dep1');";
// some code to execute sql query to data
}
@Test // Test Case
void testCase() { ... }
@AfterEach // PostConditions
void tearDown() {
String sql = "delete from department where id = 1;";
// some code to execute sql query to data
}
}
public static class TestCase2Test {
@BeforeEach // PreConditions
void setUp2() { ... }
@Test // Test Case
void testCase2() { ... }
@AfterEach // PostConditions
void tearDown2() { ... }
}
}
Такое решение позволяет выполнять методы помеченные аннотациями @BeforeEach
и @AfterEach
только для определённого теста. Но такой подход привносить сложность в разработку и такой код сложно поддерживать и читать.
Есть ли другое решение?
Решение, которое предлагает JUnit "из коробки", требует от разработчика дополнительных усилий и временных затрат на реализацию механизма выполнения запросов на этапах ПредУсловий (PreConditions) и ПостУсловий (PostConditions).
Но существует удобный инструмент, который помогает легко решать задачи такого класса:
Цели проекта DbChange
Предоставить API по удобному выполнению SQL запросов в JUnit тестах.
Упростить написание и поддержку SQL запросов в JUnit тестах.
Предоставить библиотеку, которая не зависит от различных фрейморков. (Используется только стандартная библиотека Java и JUnit 5 как compile зависимость)
Ключевые концепции DbChange
В DbChange есть три аннотации:
DbChange
DbChangeOnce
SqlExecutorGetter
DbChange
Предоставляет информацию об изменениях в данных СУБД до/после выполнения определённого тестового метода.
DbChangeOnce
Предоставляет информацию об изменениях в данных СУБД до/после выполнения всех тестовых методов в классе.
SqlExecutorGetter
Задаёт sql executor по умолчанию для всех тестов в классе. Значение в этой аннотации является имя публичного метода в тестовом классе, который возвращает экземпляр класса, реализующего интерфейс SqlExecutor
. DbChange предлагает одну реализация этого интерфейса - DefaultSqlExecutor
.
Пример расположения аннотаций в коде:
@ExtendWith(DbChangeExtension.class)
@DbChangeOnce
@SqlExecutorGetter
public class DbChangeUsageTest {
@Test
@DbChange
void test() {
}
}
Подключение библиотеки в проект
Gradle
Открыть на редактирование файл
build.gradle.kts
(илиbuild.gradle
для groovy)Добавить DbChange в зависимости проекта (пример на Kotlin):
dependencies {
testImplementation("io.github.darrmirr:dbchange:1.0.1")
}
Maven
Открыть на редактирование
pom.xml
.Добавить DbChange в зависимости.
<dependency>
<groupId>io.github.darrmirr</groupId>
<artifactId>dbchange</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
Как использовать DbChange
(обязательно) Добавить
@ExtendWith(DbChangeExtension.class)
аннотацию над тестовым классом.(обязательно) Создать публичный метод в тестовом классе, который вернёт экземпляр класса, реализующий интерфейс
SqlExecutor
. Можно воспользоваться классомDefaultSqlExecutor
.(опционально) Добавить аннотацию
@DbChangeOnce
над тестовым классом.(опционально) Добавить аннотацию
@SqlExecutorGetter
над тестовым классом.(опционально) Добавить аннотацию
@DbChange
над тестовым методом.
Примечание
DbChange не будет выполнять каких-либо действий, если в тестовом классе нет аннотаций
@DbChangeOnce
и@DbChange
.Если аннотация
@SqlExecutorGetter
не указана, то указание значенияsqlExecuterGetter
в аннотациях@DbChangeOnce
и@DbChange
– обязательно.Если используется аннотация
@DbChangeOnce
, тогда необходимо инициировать экземпляр классаjavax.sql.DataSource
в конструкторе тестового класса или в статическом контексте (например, используя@BeforeAll
JUnit аннотацию)
Простой пример использования DbChange:
@ExtendWith(DbChangeExtension.class)
@DbChangeOnce(sqlQueryFiles = "sql/database_init.sql")
@DbChangeOnce(sqlQueryFiles = "sql/database_destroy.sql", executionPhase = ExecutionPhase.AFTER_ALL)
@SqlExecutorGetter("defaultSqlExecutor")
public class DbChangeUsageTest {
private DataSource dataSource;
public DbChangeUsageTest() {
dataSource = // code to create instance of DataSource
}
public SqlExecutor defaultSqlExecutor() {
return new DefaultSqlExecutor(dataSource);
}
@Test
@DbChange(changeSet = InsertEmployee6Chained.class )
@DbChange(changeSet = DeleteEmployee6Chained.class , executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void changeSetChained() {
/* code omitted */
}
}
Рабочий процесс DbChange
Сборка информации из аннотаций
@DbChangeOnce
и@DbChange
.Генерация SQL запросов с named JDBC параметрами.
Передача сгенерированных SQL запросов на выполнение в SqlExecutor.
Отправка через JDBC драйвер шаблона запроса и JDBC параметров в СУБД на выполнение.
В DbChange cуществует несколько источников изменений в СУБД. Эти источники называются "Поставщики SQL запросов"
Поставщики SQL запросов
DbChange выполняет SQL запросы, которые поставляются в аннотациях @DbChangeOnce
и @DbChange
.
Существуют следующие поставщики SQL запросов:
statements
sql query files
changeset
sql query getter
JUnit
@MethodSource
(только для параметризированных тестов в JUnit)
Примечание
Все поставщики SQL запросов (кроме @MethodSource
) поддерживаются аннотациями @DbChangeOnce
и @DbChange
.
Рассмотрим каждого поставщика в отдельности.
Statements
Это значение в аннотации предоставляет возможность указать SQL запрос как строку:
@ExtendWith(DbChangeExtension.class)
public class ExampleTest {
@Test
@DbChange(statements = {
"insert into department(id, name) values (14, 'dep14');",
"insert into occupation(id, name) values (8, 'occ8');",
"insert into employee(id, department_id, occupation_id, first_name, last_name) values (10, 14, 8, 'Ivan', 'Ivanov')"
})
@DbChange(statements = {
"delete from employee where id = 10;",
"delete from occupation where id = 8;",
"delete from department where id = 14;"
}, executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void statements() { /* code omitted */ }
}
Плюсы:
Легко использовать
SQL запросы кодируются явно
Декларативный способ выполнения SQL запроса
Минусы:
Сложно переиспользовать SQL запросы в других тестах
Сложно читать Java код, если SQL запросов будет много или они будут содержать много переменных
Сложно кастомизировать такие SQL запросы параметрами
Сложно понять, какие именно значения нужны для конкретного теста, а какие вставляются для корректности выполнения SQL запроса (например, в силу not null ограничений)
Требуется много однотипных действий, если необходимо править все SQL запросы во всех классах
SQL query files
Это значение в аннотации предоставляет возможность указать путь до SQL файла, в котором может быть один или несколько SQL запросов.
@ExtendWith(DbChangeExtension.class)
public class ExampleTest {
@Test
@DbChange(sqlQueryFiles = { "sql/test_1_init1.sql", "sql/test_1_init2.sql" })
@DbChange(sqlQueryFiles = "sql/test_1_destroy_all.sql", executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void sqlQueryFiles() { /* code omitted */ }
}
Плюсы:
Легко использовать
Повышает читабельность кода, если используется большое количество SQL запросов
Минусы:
Сложно переиспользовать SQL запросы в других тестах
Сложно кастомизировать такие SQL запросы параметрами
Сложно понять, какие именно значения нужны для конкретного теста, а какие вставляются для корректности выполнения SQL запроса (например, в силу not null ограничений)
Требуется много однотипных действий, если необходимо править все SQL запросы во всех классах
Changeset
Это значение в аннотации предоставляет возможность указать массив классов, которые реализую интерфейс com.github.darrmirr.dbchange.sql.query.SqlQuery
. Пример использования:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class ExampleTest {
@Test
@DbChange(changeSet = { InsertDepartment9.class, InsertOccupation3.class, InsertEmployee5.class })
@DbChange(changeSet = { DeleteEmployee5.class, DeleteOccupation3.class, DeleteDepartment9.class }, executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void changeSet() { /* code omitted */ }
}
Как это работает
Все классы, которые указываются в значении changeSet, обязаны реализовывать интерфейс com.github.darrmirr.dbchange.sql.query.SqlQuery
:
import java.util.function.Supplier;
/**
* Common interface for all sql query.
*/
@FunctionalInterface
public interface SqlQuery extends Supplier<String> {
}
Это довольно простой интерфейс, в котором определён только один метод get()
(определён в интерфейсе Supplier). Этот метод возвращает SQL запрос в виде объекта Java String.
DbChange предоставляет несколько реализаций интерфейса SqlQuery
:
TemplateSqlQuery
EmptyTemplateSqlQuery
InsertSqlQuery
SpecificTemplateSqlQuery
Все перечисленные классы предоставляют возможность задать SQL запрос с named JDBC параметрами. Такой подход даёт возможность переиспользовать ранее написанный код и кастомизировать SQL запрос.
Примечание
Рекомендуется использовать InsertSqlQuery
и SpecificTemplateSqlQuery
. Использование TemplateSqlQuery
и EmptyTemplateSqlQuery
не запрещено, но эти классы создавались преимущественно для внутреннего использования.
InsertSqlQuery
Этот класс расширяет TemplateSqlQuery
класс и предоставляет возможность создать шаблон SQL запроса в зависимости от имён параметров и имени таблицы.
Класс InsertSqlQuery
абстрактный и вам необходимо расширить его, чтобы использовать в своём проекте:
public class InsertEmployee7 extends InsertSqlQuery {
@Override
public String tableName() {
return "employee";
}
@Override
public Map<String, Object> getParameters() {
Map<String, Object> params = new HashMap<>();
params.put("id", Id.EMP_7);
params.put("department_id", Id.DEP_11);
params.put("occupation_id", Id.OCC_5);
return params;
}
}
DbChange сгенерирует SQL запрос согласно данным класса InsertEmploee7
:
insert into employee(id, department_id, occupation_id) values (:id, :department_id, :occupation_id);
После этого DbChange продолжил работу по своему рабочему процессу, который был описан выше в этой статье.
SpecificTemplateSqlQuery
Этот класс также расширяет TemplateSqlQuery
класс как и InsertSqlQuery
. Вот только назначение у данного класса другое, а именно переиспользовать TemplateSqlQuery
и переопределять его SQL параметры, которые нужны для конкретного теста.
public class InsertEmployee5 extends SpecificTemplateSqlQuery {
@Override
public TemplateSqlQuery commonTemplateSqlQuery() {
return new InsertEmployeeCommon();
}
@Override
public Map<String, Object> specificParameters() {
Map<String, Object> params = new HashMap<>();
params.put("id", 5);
params.put("department_id", 9);
params.put("occupation_id", 3);
return params;
}
}
Метод commonTemplateSqlQuery()
возвращает экземпляр класса TemplateSqlQuery
. Данный объект будет использоваться как основа для создания SQL запроса. Это означает, что DbChange возьмёт из него шаблон запроса и список named JDBC параметров. Но класс SpecificTemplateSqlQuery
предоставляет нам возможность переопределять эти JDBC параметры или добавлять новые. И метод specificParameters()
как раз служит для этой цели.
Чтобы понять как это работает, посмотрим на класс, который возвращается commonTemplateSqlQuery()
методом:
public class InsertEmployeeCommon extends TemplateSqlQuery {
@Override
public String queryTemplate() {
return JdbcQueryTemplates.EMPLOYEE_INSERT;
}
@Override
public Map<String, Object> getParameters() {
Map<String, Object> params = new HashMap<>();
params.put("id", null);
params.put("department_id", null);
params.put("occupation_id", null);
params.put("first_name", "default_employee_first_name");
params.put("last_name", "default_employee_last_name");
return params;
}
}
В InsertEmployeeCommon
классе задано 5 параметров, но класс InsertEmployee5
переопределяет только 3 из них через метод specificParameters()
.
Таким образом, класс SpecificTemplateSqlQuery
предоставляет нам возможность переиспользовать ранее написанный код и упрощает добавление новых запросов и изменение существующих.
Давайте подытожим плюсы и минусы использования changeset
поставщика SQL запросов.
Плюсы:
Возможность переиспользовать код для генерации SQL запросов
Улучшенная поддержка кодовой базой по сравнению с текстовыми файлами или строками.
Проще разрабатывать и пользоваться навигацией по коду, благодаря возможностям IDE.
Возможность указать только необходимые для теста параметры, используя класс
SpecificTemplateSqlQuery
Нет необходимость "зашивать" шаблон запроса в код. Класс
InsertSqlQuery
сгенерирует его во время выполнения теста.
Минусы:
Требуется создавать отдельный файл с классом для каждого SQL запроса
Требуется наличия конструктора без аргументов
Sql query getter
Поставщик SQL запросов changeSet
предоставляет большое количество функций и преимуществ при указании SQL запросов в тестах. Но он также не лишён недостатков. И поставщик SQL запросов sqlQueryGetter
предназначен для устранения этих недостатков. Он предлагает возможность использовать конструкторы с аргументами, а также избавиться от необходимости создавать отдельные файлы под каждый класс.
Рассмотрим интерфейс SqlQueryGetter
:
/**
* Interface to supply @{@link List} of {@link SqlQuery} from method defined in test class.
*/
@FunctionalInterface
public interface SqlQueryGetter extends Supplier<List<SqlQuery>> {
}
Этот интерфейс поставляет список объектов, которые реализуют SqlQuery
интерфейс. Посмотрим на использование этого интерфейса:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class ExampleTest {
@Test
@DbChange(sqlQueryGetter = "testSqlQueryGetterInit")
@DbChange(sqlQueryGetter = "testSqlQueryGetterDestroy", executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void sqlQueryGetter() { /* code omited */ }
public SqlQueryGetter testSqlQueryGetterInit() {
return () -> Arrays.asList(
() -> "insert into department(id, name) values(3, 'dep3');",
TemplateSqlQuery
.templateBuilder(JdbcQueryTemplates.DEPARTMENT_INSERT)
.withParam(DepartmentQuery.PARAM_ID, Id.DEP_4)
.withParam(DepartmentQuery.PARAM_NAME, "dep" + Id.DEP_4)
.build()
);
}
public SqlQueryGetter testSqlQueryGetterDestroy() {
return () -> Collections.singletonList(
() -> String.format(JavaQueryTemplates.DEPARTMENT_DELETE_TWO, Id.DEP_3, Id.DEP_4)
);
}
}
Классы TemplateSqlQuery
иInsertSqlQuery
реализуют шаблон Builder. Это даёт возможность декларативно и просто создавать экземпляры этих классов без необходимости явно определять их в отдельных java файлах. В sqlQueryGetter вы можете использовать статические вложенные или анонимные классы и в них передавать зависимости. И наконец, вы можете использовать строки для задания SQL запросов.
Плюсы:
Включает все плюсы для changeset поставщика SQL запросов
Не требует использования конструктора без параметров
Не требует создания отдельного java файла для каждого SQL запроса
Минусы:
Требует создания дополнительных методов в тестовом классе
DbChange и параметризированные тесты
DbChange также предоставляет возможность выполнять SQL запросы в параметризированных тестах. Это означает, что вы можете определить для каждого набора параметров отдельный набор SQL запросов.
DbChange поддерживает только аннотацию @MethodSource
как источник SQL запросов. Рассмотрим, пример:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class ExampleTest {
@ParameterizedTest
@MethodSource("sourceStatementsParameterized")
void statementsParameterized(List<DbChangeMeta> dbChangeMetas) {
// code omitted
}
public static Stream<Arguments> sourceStatementsParameterized() {
return Stream.of(
Arguments.of(
Arrays.asList(
new DbChangeMeta()
.setStatements(Arrays.asList(
"insert into department(id, name) values (15, 'dep15');",
"insert into occupation(id, name) values (9, 'occ9');",
"insert into employee(id, department_id, occupation_id, first_name, last_name) values (11, 15, 9, 'Ivan', 'Ivanov')"
))
.setExecutionPhase(DbChangeMeta.ExecutionPhase.BEFORE_TEST),
new DbChangeMeta()
.setStatements(Arrays.asList(
"delete from employee where id = 11;",
"delete from occupation where id = 9;",
"delete from department where id = 15"
))
.setExecutionPhase(DbChangeMeta.ExecutionPhase.AFTER_TEST)
)
),
Arguments.of(
Arrays.asList(
new DbChangeMeta()
.setStatements(Arrays.asList(
"insert into department(id, name) values (16, 'dep16');",
"insert into occupation(id, name) values (10, 'occ10');",
"insert into employee(id, department_id, occupation_id, first_name, last_name) values (12, 16, 10, 'Petr', 'Petrov')"
))
.setExecutionPhase(DbChangeMeta.ExecutionPhase.BEFORE_TEST),
new DbChangeMeta()
.setStatements(Arrays.asList(
"delete from employee where id = 12;",
"delete from occupation where id = 10;",
"delete from department where id = 16;"
))
.setExecutionPhase(DbChangeMeta.ExecutionPhase.AFTER_TEST)
)
)
);
}
}
Во-первых, обратите внимание, что аннотация @DbChange
не используется в параметризованных тестах. Вы можете поставить эту аннотацию над методом, но SQL запросы из неё будут выполняться для каждого набора аргументов в параметризованном тесте.
Во-вторых, вы обязаны указать List<DbChangeMeta> dbChangeMetas
в аргументах тестового метода. Это обязательно, так требует внутренняя реализация JUnit.
Что такое DbChangeMeta?
DbChangeMeta - это класс в DbChange JUnit расширении. Во время работы DbChange конвертирует всю информацию из аннотаций @DbChange
и @DbChangeOnce
в экземпляры класса DbChangeMeta
. Это происходит на первом шаге рабочего процесса DbChange. В подавляющем большинстве случаев разработчик, использующий DbChange, работает только с аннотациями @DbChange
и @DbChangeOnce
. Но существует одно исключение из этого правила - это параметризованный тест.
DbChange ожидает, что список объектов DbChangeMeta
будет передан в одном из аргументов тестового метода. DbChange ничего не будет делать во время выполнения параметризированного теста, если такой список объектов отсутствует.
Класс DbChangeMeta
имеет туже структуру, что и аннотации @DbChange
и @DbChangeOnce
. И все правила использования поставщиками SQL запросов справедливы и для класса DbChangeMeta
.
Связанные (chained) SQL запросы
Рассмотрим пример:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class ExampleTest {
@Test
@DbChange(changeSet = { InsertDepartment9.class, InsertOccupation3.class, InsertEmployee5.class })
@DbChange(changeSet = { DeleteEmployee5.class, DeleteOccupation3.class, DeleteDepartment9.class }, executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void changeSet() { /* code omitted */ }
}
Из данного кода, к сожалению, не очевидно, что InsertEmployee5.class
зависит от InsertOccupation3.class
иInsertDepartment9.class
. И если изменить порядок в массиве, например поставить InsertEmployee5.class
в самое начало, то выполнение теста завершится брошенным исключением. Причина ошибка заключается в том, что при попытке вставить новую запись в таблицу employee, СУБД вернёт ошибку, что департамент и профессия для данного сотрудника не найдены в соответствующих таблицах. А отсутствуют они из-за не корректного порядка выполнения SQL запросов.
DbChange предоставляет возможность связать (chain) такие запросы в цепочку и выполнять их в нужной последовательности. Рассмотрим интерфейс, позволяющий выполнить такое связывание:
@FunctionalInterface
public interface ChainedSqlQuery {
/**
* Get next instance of {@link SqlQuery} that relates to current one.
*
* @return instance of {@link SqlQuery}.
*/
SqlQuery next();
}
Примечание
Такая возможность доступна только для changeset и sqlQueryGetter поставщиков SQL запросов.
Интерфейс ChainedSqlQuery
довольно простой. У него только один метод next()
. Рассмотрим, пример использования интерфейса:
public class InsertEmployee5Chained extends SpecificTemplateSqlQuery implements ChainedSqlQuery {
@Override
public TemplateSqlQuery commonTemplateSqlQuery() {
return new InsertDepartmentCommon();
}
@Override
public Map<String, Object> specificParameters() {
return Collections.singletonMap(DepartmentQuery.PARAM_ID, Id.DEP_9);
}
@Override
public SqlQuery next() {
return new InsertOccupation3();
}
public static class InsertOccupation3 extends SpecificTemplateSqlQuery implements ChainedSqlQuery {
@Override
public TemplateSqlQuery commonTemplateSqlQuery() {
return new InsertOccupationCommon();
}
@Override
public Map<String, Object> specificParameters() {
return Collections.singletonMap("id", Id.OCC_3);
}
@Override
public SqlQuery next() {
return new InsertEmployee5();
}
}
public static class InsertEmployee5 extends SpecificTemplateSqlQuery {
@Override
public TemplateSqlQuery commonTemplateSqlQuery() {
return new InsertEmployeeCommon();
}
@Override
public Map<String, Object> specificParameters() {
Map<String, Object> params = new HashMap<>();
params.put("id", Id.EMP_5);
params.put("department_id", Id.DEP_9);
params.put("occupation_id", Id.OCC_3);
return params;
}
}
}
Здесь довольно много строчек кода, рассмотрим их более подробно.
Класс InsertEmployee5Chained
расширяет SpecificTemplateSqlQuery
и переиспользует SQL запрос, определённый в классе InsertDepartmentCommon
. Дополнительно InsertEmployee5Chained
переопределяет некоторые JDBC параметры в запросе на вставку данных в таблицу с департаментами.
Возможно, это выглядит странным, что имя класса говорит о вставке данных по сотруднику, а в действительности класс содержит информацию для SQL запроса на вставку данных по департаменту. Во-первых, согласно бизнес модели примера, нельзя вставить данные по сотруднику без данных по департаменту, которому данный сотрудник принадлежит. Во-вторых, не стоит забывать, что определение класса - это не только его методы и переменные. У класса ещё могут быть вложенные классы. И в приведённом примере их два: InsertOccupation3
и InsertEmployee5
.
Как DbChange поймёт, в какой последовательности выполнять SQL запросы, определённые в этих классах?
Вот здесь в дело вступает ChainedSqlQuery
интерфейс. Его метод next()
указывает на следующий выполняемый SQL запрос. В приведённом примере - это InsertOccupation3
.
Заметьте, что класс InsertOccupation3
тоже реализует ChainedSqlQuery
интерфейс. Где указывается, что следующий выполняемый SQL запрос - InsertEmployee5
.
Таким образом, цепочка состоит из 3-х SQL запросов:
insert department 9 -> insert occupation 3 -> insert employee 5
Примечание
Вы можете связывать столько SQL запросов, сколько вам необходимо. Размер цепочки ограничен только размером стека потока, который определён в вашей виртуальной машине Java (JVM)
И наконец, внесём изменения в аннотацию @DbChange
в примере с которого мы начинали рассматривать связанные SQL запросы:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class ExampleTest {
@Test
@DbChange(changeSet = InsertEmployee5Chained.class )
@DbChange(changeSet = DeleteEmployee5Chained.class, executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void changeSet() { /* code omitted */ }
}
Как вы видите, количество классов в массиве changeSet уменьшилось с 3-х до одного. И теперь chained классы содержат необходимую цепочку SQL запросов для выполнения их в требуемом порядке.
DbChange фазы выполнения
Возможно вы обратили внимание на значения executionPhase
в аннотациях @DbChange
или@DbChangeOnce
.
Фаза выполнения описывает момент времени в процессе прогона теста, в который необходимо выполнить SQL запрос. Фазы выполнения, определённые в DbChange, полностью совпадают с фазами, определёнными в JUnit.
С моей точки зрения, имена фаз, хорошо описывают момент времени, когда будет выполнен SQL запрос. Но если имена фаз для Вас не понятны, то, пожалуйста, обратитесь к официальной документации JUnit.
SqlExecutorGetter
Обычно приложение использует только один экземпляр класса javax.sql.DataSource
для подключения к СУБД. Но иногда приложение работает с несколькими схемами или с несколькими БД одновременно. И по этой причине в приложении может быть проинициализировано несколько экземпляров класса javax.sql.DataSource
.
DbChange предоставляет возможность указать SqlExecutor
в аннотации @DbChange
и@DbChangeOnce
. Для этой цели используется значение sqlExecutorGetter
. В этом значении необходимо указать имя публичного метода, определённого в тестовом классе. Этот метод должен возвращать экземпляр класса, который реализует интерфейс com.github.darrmirr.dbchange.sql.executor.SqlExecutor
.
Рассмотрим пример:
@ExtendWith(DbChangeExtension.class)
@SqlExecutorGetter("defaultSqlExecutor")
public class DbChangeUsageTest {
private DataSource dataSource1;
private DataSource dataSource2;
public DbChangeUsageTest() {
dataSource1 = // code to create instance of DataSource
dataSource2 = // code to create instance of DataSource
}
public SqlExecutor defaultSqlExecutor() {
return new DefaultSqlExecutor(dataSource1);
}
public SqlExecutor datasource2SqlExecutor() {
return new DefaultSqlExecutor(dataSource2);
}
@Test
@DbChange(changeSet = InsertEmployee6Chained.class)
@DbChange(changeSet = DeleteEmployee6Chained.class , executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
@DbChange(changeSet = InsertBankList.class, sqlExecutorGetter = "datasource2SqlExecutor")
@DbChange(changeSet = DeleteBankList.class, sqlExecutorGetter = "datasource2SqlExecutor", executionPhase = DbChange.ExecutionPhase.AFTER_TEST)
void test() {
/* code omitted */
}
}
DbChange возьмёт экземпляр класса, реализующий интерфейс SqlExecutor
, из метода datasource2SqlExecutor
для выполнения запросов InsertBankList
и DeleteBankList
. Значение sqlExecutorGetter
в аннотациях @DbChange
или@DbChangeOnce
всегда переопределяет значение в аннотации @SqlExecutorGetter
.
Примечание
Если аннотация @SqlExecutorGetter
не определена в тестовом классе, то указание значения в sqlExecutorGetter
в каждой аннотации @DbChange
и@DbChangeOnce
- обязательно.
Заключение
DbChange является расширением JUnit 5, которое предоставляет возможность декларативно указать SQL запросы и выполнить их на стадиях ПредУсловия (PreCondition) и ПостУсловия (PostCondition).
DbChange репозиторий доступен на Github.com.
Примеры использования расширения можно посмотреть в классе com.github.darrmirr.dbchange.component.DbChangeUsageTest
в кодовой базе проекта.