Введение в DbChange JUnit расширение

от автора

Простой и декларативный способ выполнять sql запросы в JUnit тестах.

Введение

Структура JUnit теста следует модели тестового сценария (test case):

ПредУсловия (PreConditions) — это действия, которые переводят тестируемую систему в определённое состояние необходимое для выполнения тестового сценария.

Тестовый сценарий (Test case) — это действия, которые меняют состояние тестируемой системы с целью сверить действительное поведение системы с ожидаемым.

ПостУсловия (PostConditions) — это действия, которые переводят тестируемую систему в первоначальное состояние, которое было до выполнения ПредУсловий.

JUnit предоставляет соответствующие аннотации согласно модели тестового сценария:

  1. ПредУсловия (PreConditions) = @BeforeEach

  2. Тестовый сценарий (Test case) = @Test

  3. ПостУсловия (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

  1. Предоставить API по удобному выполнению SQL запросов в JUnit тестах.

  2. Упростить написание и поддержку SQL запросов в JUnit тестах.

  3. Предоставить библиотеку, которая не зависит от различных фрейморков. (Используется только стандартная библиотека Java и JUnit 5 как compile зависимость)

Ключевые концепции DbChange

В DbChange есть три аннотации:

  1. DbChange

  2. DbChangeOnce

  3. SqlExecutorGetter

DbChange
Предоставляет информацию об изменениях в данных СУБД до/после выполнения определённого тестового метода.

DbChangeOnce
Предоставляет информацию об изменениях в данных СУБД до/после выполнения всех тестовых методов в классе.

SqlExecutorGetter
Задаёт sql executor по умолчанию для всех тестов в классе. Значение в этой аннотации является имя публичного метода в тестовом классе, который возвращает экземпляр класса, реализующего интерфейс SqlExecutor. DbChange предлагает одну реализация этого интерфейса — DefaultSqlExecutor.

Пример расположения аннотаций в коде:

@ExtendWith(DbChangeExtension.class) @DbChangeOnce @SqlExecutorGetter public class DbChangeUsageTest {          @Test     @DbChange     void test() {     } }

Подключение библиотеки в проект

Предусловия:

  1. Создать директорию libs в корне проекта.

  2. Скачать JAR файл последней версии DbChange cо страницы релизов проекта на Github.com.

  3. Скопировать скаченный JAR файл в директорию libs.

Gradle

  1. Открыть на редактирование файл build.gradle.kts (или build.gradle для groovy)

  2. Добавить flatDir в секцию repository (пример на Kotlin):

repository {     flatDir{         dirs('libs')     } }
  1. Добавить DbChange в зависимости проекта:

dependencies {     testImplementation 'com.github.darrmirr:dbchange:1.0.0' }
  1. Выполнить команду build проекта

Maven

  1. Открыть на редактирование pom.xml.

  2. Добавить DbChange в зависимости.

 <dependecy>     <groupId>com.github.darrmirr</groupId>     <artifactId>dbchange</artifactId>     <version>1.0.0</version>     <scope>test</scope>   </dependecy>
  1. Добавить 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>
  1. Выполнить команду mvn compile.

Как использовать DbChange

  1. (обязательно) Добавить @ExtendWith(DbChangeExtension.class) аннотацию над тестовым классом.

  2. (обязательно) Создать публичный метод в тестовом классе, который вернёт экземпляр класса, реализующий интерфейс SqlExecutor. Можно воспользоваться классом DefaultSqlExecutor.

  3. (опционально) Добавить аннотацию @DbChangeOnce над тестовым классом.

  4. (опционально) Добавить аннотацию @SqlExecutorGetter над тестовым классом.

  5. (опционально) Добавить аннотацию @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

  1. Сборка информации из аннотаций @DbChangeOnce и @DbChange.

  2. Генерация SQL запросов с named JDBC параметрами.

  3. Передача сгенерированных SQL запросов на выполнение в SqlExecutor.

  4. Отправка через 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *