Введение в DbChange JUnit расширение
https://t.me/javalibПростой и декларативный способ выполнять 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 запросами в зависимости от названия метода.
Как решить эту проблему?
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в конструкторе тестового класса или в статическом контексте (например, используя@BeforeAllJUnit аннотацию)
Простой пример использования 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 в кодовой базе проекта.