«1. Обзор

В этой статье мы рассмотрим модель расширения в библиотеке тестирования JUnit 5. Как следует из названия, целью расширений Junit 5 является расширение поведения тестовых классов или методов, и их можно повторно использовать для нескольких тестов.

До Junit 5 версия библиотеки JUnit 4 использовала два типа компонентов для расширения теста: средства выполнения тестов и правила. Для сравнения, JUnit 5 упрощает механизм расширения, вводя единую концепцию: Extension API.

2. Модель расширения JUnit 5

Расширения JUnit 5 связаны с определенным событием при выполнении теста, называемым точкой расширения. Когда достигается определенная фаза жизненного цикла, механизм JUnit вызывает зарегистрированные расширения.

Можно использовать пять основных типов точек расширения:

    постобработка экземпляра теста условное выполнение теста обратные вызовы жизненного цикла разрешение параметров обработка исключений

Мы рассмотрим каждый из них более подробно в следующих разделах. .

3. Зависимости Maven

Во-первых, давайте добавим зависимости проекта, которые нам понадобятся для наших примеров. Основная библиотека JUnit 5, которая нам понадобится, это junit-jupiter-engine:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>

Также давайте добавим две вспомогательные библиотеки для использования в наших примерах:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
</dependency>

Последние версии junit-jupiter- engine, h2 и log4j-core можно загрузить с Maven Central.

4. Создание расширений JUnit 5

Чтобы создать расширение JUnit 5, нам нужно определить класс, реализующий один или несколько интерфейсов, соответствующих точкам расширения JUnit 5. Все эти интерфейсы расширяют основной интерфейс расширения, который является только интерфейсом маркера.

4.1. Расширение TestInstancePostProcessor

Этот тип расширения выполняется после создания экземпляра теста. Интерфейсом для реализации является TestInstancePostProcessor, который имеет метод postProcessTestInstance() для переопределения.

Типичным вариантом использования этого расширения является внедрение зависимостей в экземпляр. Например, давайте создадим расширение, которое создает экземпляр объекта логгера, а затем вызывает метод setLogger() для тестового экземпляра:

public class LoggingExtension implements TestInstancePostProcessor {

    @Override
    public void postProcessTestInstance(Object testInstance, 
      ExtensionContext context) throws Exception {
        Logger logger = LogManager.getLogger(testInstance.getClass());
        testInstance.getClass()
          .getMethod("setLogger", Logger.class)
          .invoke(testInstance, logger);
    }
}

Как видно выше, метод postProcessTestInstance() обеспечивает доступ к тестовому экземпляру и вызывает метод setLogger() тестового класса с использованием механизма отражения.

4.2. Условное выполнение теста

JUnit 5 предоставляет тип расширения, которое может контролировать, следует ли запускать тест. Это определяется реализацией интерфейса ExecutionCondition.

Давайте создадим класс EnvironmentExtension, который реализует этот интерфейс и переопределяет метод AssessmentExecutionCondition().

Метод проверяет, равно ли свойство, представляющее имя текущей среды, «qa», и отключает тест в этом случае:

public class EnvironmentExtension implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(
      ExtensionContext context) {
        
        Properties props = new Properties();
        props.load(EnvironmentExtension.class
          .getResourceAsStream("application.properties"));
        String env = props.getProperty("env");
        if ("qa".equalsIgnoreCase(env)) {
            return ConditionEvaluationResult
              .disabled("Test disabled on QA environment");
        }
        
        return ConditionEvaluationResult.enabled(
          "Test enabled on QA environment");
    }
}

В результате тесты, регистрирующие это расширение, не будут запускаться на « «qa» среда.

Если мы не хотим, чтобы условие проверялось, мы можем деактивировать его, установив ключ конфигурации junit.conditions.deactivate на шаблон, соответствующий условию.

Этого можно добиться, запустив JVM со свойством -Djunit.conditions.deactivate=\u003cpattern\u003e или добавив параметр конфигурации в LauncherDiscoveryRequest:

public class TestLauncher {
    public static void main(String[] args) {
        LauncherDiscoveryRequest request
          = LauncherDiscoveryRequestBuilder.request()
          .selectors(selectClass("com.baeldung.EmployeesTest"))
          .configurationParameter(
            "junit.conditions.deactivate", 
            "com.baeldung.extensions.*")
          .build();

        TestPlan plan = LauncherFactory.create().discover(request);
        Launcher launcher = LauncherFactory.create();
        SummaryGeneratingListener summaryGeneratingListener
          = new SummaryGeneratingListener();
        launcher.execute(
          request, 
          new TestExecutionListener[] { summaryGeneratingListener });
 
        System.out.println(summaryGeneratingListener.getSummary());
    }
}

4.3. Обратные вызовы жизненного цикла

Этот набор расширений связан с событиями в жизненном цикле теста и может быть определен путем реализации следующих интерфейсов:

    BeforeAllCallback и AfterAllCallback — выполняются до и после выполнения всех тестовых методов. BeforeEachCallBack и AfterEachCallback — – выполняется до и после каждого тестового метода. BeforeTestExecutionCallback и AfterTestExecutionCallback – выполняется непосредственно перед и сразу после тестового метода. определите класс, который реализует некоторые из этих интерфейсов и управляет поведением теста, который обращается к базе данных с помощью JDBC.

Во-первых, давайте создадим простую сущность Employee:

  1. BeforeAllCallback
  2. BeforeAll
  3. BeforeEachCallback
  4. BeforeEach
  5. BeforeTestExecutionCallback
  6. Test
  7. AfterTestExecutionCallback
  8. AfterEach
  9. AfterEachCallback
  10. AfterAll
  11. AfterAllCallback

«

«Нам также понадобится служебный класс, который создает Connection на основе файла .properties:

public class Employee {

    private long id;
    private String firstName;
    // constructors, getters, setters
}

Наконец, давайте добавим простой DAO на основе JDBC, который управляет записями сотрудников:

public class JdbcConnectionUtil {

    private static Connection con;

    public static Connection getConnection() 
      throws IOException, ClassNotFoundException, SQLException{
        if (con == null) {
            // create connection
            return con;
        }
        return con;
    }
}

Давайте создадим наше расширение, которое реализует некоторые интерфейсы жизненного цикла:

public class EmployeeJdbcDao {
    private Connection con;

    public EmployeeJdbcDao(Connection con) {
        this.con = con;
    }

    public void createTable() throws SQLException {
        // create employees table
    }

    public void add(Employee emp) throws SQLException {
       // add employee record
    }

    public List<Employee> findAll() throws SQLException {
       // query all employee records
    }
}

Каждый из этих интерфейсов содержит метод, который нам нужно переопределить.

public class EmployeeDatabaseSetupExtension implements 
  BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
    //...
}

Для интерфейса BeforeAllCallback мы переопределим метод beforeAll() и добавим логику для создания таблицы сотрудников перед выполнением любого тестового метода:

Далее мы будем использовать BeforeEachCallback и AfterEachCallback для оберните каждый тестовый метод в транзакцию. Целью этого является откат любых изменений в базе данных, выполненных в тестовом методе, чтобы следующий тест выполнялся на чистой базе данных.

private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();

@Override
public void beforeAll(ExtensionContext context) throws SQLException {
    employeeDao.createTable();
}

В методе beforeEach() мы создадим точку сохранения, используемую для отката состояния базы данных до:

Затем в методе afterEach() мы откатим базу данных изменения, сделанные во время выполнения тестового метода:

private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;

@Override
public void beforeEach(ExtensionContext context) throws SQLException {
    con.setAutoCommit(false);
    savepoint = con.setSavepoint("before");
}

Чтобы закрыть соединение, воспользуемся методом afterAll(), выполняемым после завершения всех тестов:

@Override
public void afterEach(ExtensionContext context) throws SQLException {
    con.rollback(savepoint);
}

4.4. Разрешение параметра

@Override
public void afterAll(ExtensionContext context) throws SQLException {
    if (con != null) {
        con.close();
    }
}

Если конструктор теста или метод получает параметр, он должен быть разрешен во время выполнения с помощью ParameterResolver.

Давайте определим наш собственный ParameterResolver, который разрешает параметры типа EmployeeJdbcDao:

Наш распознаватель реализует интерфейс ParameterResolver и переопределяет методы supportsParameter() и resolveParameter(). Первый из них проверяет тип параметра, а второй определяет логику для получения экземпляра параметра.

public class EmployeeDaoParameterResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, 
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.getParameter().getType()
          .equals(EmployeeJdbcDao.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, 
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return new EmployeeJdbcDao();
    }
}

4.5. Обработка исключений

И последнее, но не менее важное: интерфейс TestExecutionExceptionHandler можно использовать для определения поведения теста при возникновении определенных типов исключений.

Например, мы можем создать расширение, которое будет регистрировать и игнорировать все исключения типа FileNotFoundException при повторном создании любого другого типа:

5. Регистрация расширений

public class IgnoreFileNotFoundExceptionExtension 
  implements TestExecutionExceptionHandler {

    Logger logger = LogManager
      .getLogger(IgnoreFileNotFoundExceptionExtension.class);
    
    @Override
    public void handleTestExecutionException(ExtensionContext context,
      Throwable throwable) throws Throwable {

        if (throwable instanceof FileNotFoundException) {
            logger.error("File not found:" + throwable.getMessage());
            return;
        }
        throw throwable;
    }
}

Теперь, когда мы определили наши тестовые расширения, нам нужно зарегистрировать их с помощью теста JUnit 5. Для этого мы можем использовать аннотацию @ExtendWith.

Аннотацию можно добавить в тест несколько раз или получить список расширений в качестве параметра:

Мы видим, что в нашем тестовом классе есть конструктор с параметром EmployeeJdbcDao, который будет разрешен путем расширения Расширение EmployeeDaoParameterResolver.

@ExtendWith({ EnvironmentExtension.class, 
  EmployeeDatabaseSetupExtension.class, EmployeeDaoParameterResolver.class })
@ExtendWith(LoggingExtension.class)
@ExtendWith(IgnoreFileNotFoundExceptionExtension.class)
public class EmployeesTest {
    private EmployeeJdbcDao employeeDao;
    private Logger logger;

    public EmployeesTest(EmployeeJdbcDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    @Test
    public void whenAddEmployee_thenGetEmployee() throws SQLException {
        Employee emp = new Employee(1, "john");
        employeeDao.add(emp);
        assertEquals(1, employeeDao.findAll().size());   
    }
    
    @Test
    public void whenGetEmployees_thenEmptyList() throws SQLException {
        assertEquals(0, employeeDao.findAll().size());   
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

При добавлении EnvironmentExtension наш тест будет выполняться только в среде, отличной от «qa».

В нашем тесте также будет создана таблица сотрудников, и каждый метод будет заключен в транзакцию путем добавления EmployeeDatabaseSetupExtension. Даже если сначала выполняется тест whenAddEmployee_thenGetEmploee(), добавляющий в таблицу одну запись, второй тест найдет в таблице 0 записей.

Экземпляр регистратора будет добавлен в наш класс с помощью LoggingExtension.

Наконец, наш тестовый класс будет игнорировать все экземпляры FileNotFoundException, так как он добавляет соответствующее расширение.

5.1. Автоматическая регистрация расширения

Если мы хотим зарегистрировать расширение для всех тестов в нашем приложении, мы можем сделать это, добавив полное имя в /META-INF/services/org.junit.jupiter.api.extension.Extension. file:

Чтобы этот механизм был включен, нам также необходимо установить ключ конфигурации junit.jupiter.extensions.autodetection.enabled в значение true. Это можно сделать, запустив JVM со свойством Djunit.jupiter.extensions.autodetection.enabled=true или добавив параметр конфигурации в LauncherDiscoveryRequest:

com.baeldung.extensions.LoggingExtension

5.2. Программная регистрация расширений

LauncherDiscoveryRequest request
  = LauncherDiscoveryRequestBuilder.request()
  .selectors(selectClass("com.baeldung.EmployeesTest"))
  .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true")
.build();

Хотя регистрация расширений с использованием аннотаций является более декларативным и ненавязчивым подходом, у нее есть существенный недостаток: мы не можем легко настроить поведение расширения. Например, при текущей модели регистрации расширений мы не можем принять свойства подключения к базе данных от клиента.

«В дополнение к декларативному подходу, основанному на аннотациях, JUnit предоставляет API для программной регистрации расширений. Например, мы можем модифицировать класс JdbcConnectionUtil, чтобы он принимал свойства подключения:

Кроме того, мы должны добавить новый конструктор для расширения EmployeeDatabaseSetupExtension для поддержки настраиваемых свойств базы данных:

public class JdbcConnectionUtil {

    private static Connection con;

    // no-arg getConnection

    public static Connection getConnection(String url, String driver, String username, String password) {
        if (con == null) {
            // create connection 
            return con;
        }

        return con;
    }
}

Теперь, чтобы зарегистрировать employee с настраиваемыми свойствами базы данных, мы должны аннотировать статическое поле аннотацией @RegisterExtension:

public EmployeeDatabaseSetupExtension(String url, String driver, String username, String password) {
    con = JdbcConnectionUtil.getConnection(url, driver, username, password);
    employeeDao = new EmployeeJdbcDao(con);
}

Здесь мы подключаемся к базе данных H2 в памяти для запуска тестов.

@ExtendWith({EnvironmentExtension.class, EmployeeDaoParameterResolver.class})
public class ProgrammaticEmployeesUnitTest {

    private EmployeeJdbcDao employeeDao;

    @RegisterExtension 
    static EmployeeDatabaseSetupExtension DB =
      new EmployeeDatabaseSetupExtension("jdbc:h2:mem:AnotherDb;DB_CLOSE_DELAY=-1", "org.h2.Driver", "sa", "");

    // same constrcutor and tests as before
}

5.3. Порядок регистрации

JUnit регистрирует статические поля @RegisterExtension после регистрации расширений, которые декларативно определены с помощью аннотации @ExtendsWith. Мы также можем использовать нестатические поля для программной регистрации, но они будут зарегистрированы после создания экземпляра тестового метода и постпроцессоров.

Если мы зарегистрируем несколько расширений программно, через @RegisterExtension, JUnit зарегистрирует эти расширения в детерминированном порядке. Хотя упорядочение является детерминированным, алгоритм, используемый для упорядочения, является неочевидным и внутренним. Чтобы обеспечить определенный порядок регистрации, мы можем использовать аннотацию @Order:

Здесь расширения упорядочены на основе приоритета, где более низкое значение имеет более высокий приоритет, чем более высокое значение. Кроме того, расширения без аннотации @Order будут иметь самый низкий возможный приоритет.

public class MultipleExtensionsUnitTest {

    @Order(1) 
    @RegisterExtension 
    static EmployeeDatabaseSetupExtension SECOND_DB = // omitted

    @Order(0)
    @RegisterExtension     
    static EmployeeDatabaseSetupExtension FIRST_DB = // omitted

    @RegisterExtension     
    static EmployeeDatabaseSetupExtension LAST_DB = // omitted

    // omitted
}

6. Заключение

В этом руководстве мы показали, как мы можем использовать модель расширения JUnit 5 для создания пользовательских тестовых расширений.

Полный исходный код примеров можно найти на GitHub.

«