Простой и декларативный способ выполнять 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 выполняет его для всех тестовых методов определённых в классе.
Как решить эту проблему?
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() { } }
Подключение библиотеки в проект
Предусловия:
-
Создать директорию
libs
в корне проекта. -
Скачать JAR файл последней версии DbChange cо страницы релизов проекта на Github.com.
-
Скопировать скаченный JAR файл в директорию
libs
.
Gradle
-
Открыть на редактирование файл
build.gradle.kts
(илиbuild.gradle
для groovy) -
Добавить
flatDir
в секциюrepository
(пример на Kotlin):
repository { flatDir{ dirs('libs') } }
-
Добавить DbChange в зависимости проекта:
dependencies { testImplementation 'com.github.darrmirr:dbchange:1.0.0' }
-
Выполнить команду build проекта
Maven
-
Открыть на редактирование
pom.xml
. -
Добавить DbChange в зависимости.
<dependecy> <groupId>com.github.darrmirr</groupId> <artifactId>dbchange</artifactId> <version>1.0.0</version> <scope>test</scope> </dependecy>
-
Добавить maven install plugin.
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-install-plugin</artifactId> <executions> <execution> <id>install-dbchange</id> <phase>generate-sources</phase> <goals> <goal>install-file</goal> </goals> <configuration> <file>${basedir}/libs/dbchange-1.0.0.jar</file> </configuration> </execution> </executions> </plugin>
-
Выполнить команду mvn compile.
Как использовать 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
в кодовой базе проекта.
ссылка на оригинал статьи https://habr.com/ru/post/684692/
Добавить комментарий