JUnit - одна из самых популярных сред модульного тестирования в экосистеме Java. Версия JUnit 5 (также известная как Jupiter) содержит множество интересных нововведений, включая поддержку новых функций в Java 8 и выше. Однако многие разработчики по-прежнему предпочитают использовать среду JUnit 4, поскольку некоторые функции, такие как параллельное выполнение в JUnit 5 все еще находятся в экспериментальной фазе.

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

По моему опыту, JUnit 5 - лучшая версия JUnit. Новая структура также включает расширяемую архитектуру и совершенно новую модель расширений, которая упрощает реализацию пользовательских функций.

В этом руководстве по JUnit мы подробно рассмотрим расширения JUnit 5 - одну из основных функций платформы JUnit 5.

СОДЕРЖАНИЕ

  • Что такого хорошего в JUnit 5?

  • Архитектура JUnit 5

  • Как зарегистрировать расширения JUnit 5

  • Условное выполнение теста JUnit 5 с аннотациями

  • Как создать расширения JUnit 5 путем реализации TestInstanceFactory

  • Как протестировать обратные вызовы жизненного цикла в JUnit 5

  • Постобработка тестового экземпляра в JUnit 5

  • Обратный вызов перед уничтожением тестового экземпляра в JUnit 5

  • Разрешение параметра в JUnit 5

  • Обработка исключений в JUnit 5

  • Сторонние расширения фреймворка в JUnit 5

Что такого хорошего в JUnit 5?

Если вы использовали фреймворк JUnit 4, вы согласитесь, что существуют ограниченные (или минимальные) возможности расширения или настройки фреймворка JUnit 4. Это одно из самых узких мест в этой версии фреймворка JUnit. В JUnit 4 такие расширения, как Runners, можно создавать, просто аннотируя тестовый класс с помощью @RunWith (MyRunner.class), чтобы JUnit мог их использовать.

Обратной стороной этого подхода является то, что вы используете только один Runner для тестового класса. Это затрудняет составление нескольких бегунов. Однако недостатки, создаваемые Runners с JUnit 4, можно преодолеть с помощью следующих параметров:

  • JUnit 4 использует правила в дополнение к Runners, что предоставляет вам гибкое решение для добавления или переопределения поведения каждого метода тестирования.

  • Можно создавать правила для аннотирования полей тестового класса. Однако у Rules есть проблема постоянства. Проще говоря, правила могут выполняться только до и после запуска теста, но не могут быть реализованы внутри теста.

Итак, как среда JUnit 5 решает эту давнюю проблему JUnit 4? JUnit 5 предлагает механизм расширения, который открывает сторонние инструменты или API через модель расширения. Он состоит из единой и последовательной концепции API-интерфейсов расширения для преодоления ограничений конкурирующих точек расширения JUnit 4 (например, Runner, TestRule и MethodRule).

Теперь, когда мы рассмотрели суть расширений JUnit 5, вот ряд вопросов, которые возникают у разработчиков Java:

  • Почему мы должны использовать расширения?

  • Сколько усилий требуется для создания расширения JUnit 5?

  • Модель расширения лучше, чем «Модель программирования»?

Вот что упоминается в основных принципах JUnit 5:

Лучше включить новую функциональность, создав или расширив точку расширения, а не добавляя функциональность как основную.

Архитектура JUnit 5

Предыдущие версии фреймворка JUnit (т.е. до JUnit 4) поставлялись в одном jar файле. Однако JUnit 5 архитектурно отличается от более ранних версий JUnit. Поэтому JUnit 5 поставляется в разных модулях, используя новую архитектуру, которая разделяет API, механизм выполнения, выполнение и интеграцию.

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

  1. Платформа JUnit: предоставляет API для инструментов обнаружения и запуска тестов. Он определяет интерфейс между JUnit и клиентами, которые хотят запускать тесты из IDE, инструментов сборки или консоли.

  2. JUnit Jupiter: предоставляет API на основе аннотаций для написания модульных тестов JUnit 5, а также механизм тестирования, который позволяет их запускать.

  3. JUnit Vintage: предлагает механизм тестирования для запуска тестов JUnit 3 и JUnit 4, тем самым обеспечивая обратную совместимость (с более ранними версиями платформы JUnit).

Цель этой архитектуры - разделить обязанности по тестированию, выполнению и расширению. Это также облегчает интеграцию других тестовых фреймворков с фреймворком JUnit.

Модель программирования или Модель расширения

Если вы QA-инженер, который регулярно пишет тесты, вы обязательно будете использовать модель программирования. С другой стороны, модель расширения предоставляет несколько интерфейсов в виде API-интерфейсов расширений, которые могут быть реализованы поставщиками расширений (разработчиками или поставщиками инструментов) для расширения основных функций JUnit 5.

Архитектура JUnit 5

Как видно из архитектуры JUnit 5, показанной выше, модель расширения является частью модуля Jupiter, который позволяет вам расширять основные функции JUnit 5 с помощью гибких и мощных расширений. Кроме того, расширение JUnit 5 преодолевает ограничения расширения JUnit 4, заменяя его механизмы расширения Runners и Rules. Наконец, поскольку JUnit 5 обеспечивает обратную совместимость, вы все равно можете запускать тесты JUnit 4 с JUnit 5 .

Модель расширения JUnit Jupiter предоставляется через небольшой интерфейс в пакете org.junit.jupiter.api.extension, который может использоваться разработчиками или поставщиками расширений.

Теперь, когда мы рассмотрели основы расширений JUnit 5, давайте напишем код примера, который иллюстрирует расширения JUnit 5. Для этого давайте создадим Java-проект с тремя примерами тестов для Java-класса, используя Eclipse IDE:

Если вы работаете в других Java IDE (не Eclipse), вы можете ознакомиться с подробным блог постом, в котором подробно рассказывается, как запустить JUnit из Eclipse IDE . После добавления библиотеки JUnit 5 в путь сборки (или добавления зависимостей для проекта Maven) мы видим, что расширение JUnit 5 находится в org.junit.jupiter.api в пакете org.junit.jupiter.api.extension как показано ниже:

Вот пример Java кода, демонстрирующий простое расширение JUnit 5:

class FirstTestCase {
 
    @BeforeAll
    static void setUpBeforeClass() throws Exception {
    }
    @AfterAll
    static void tearDownAfterClass() throws Exception {
    }
    @BeforeEach
    void setUp() throws Exception {
    }
    @AfterEach
    void tearDown() throws Exception {
    }
    @Test
    void test() {
        fail("Not yet implemented");
    }
}

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

Как зарегистрировать расширения JUnit 5

Регистрация одного или нескольких расширений в JUnit 5 выполняется через механизм Java ServiceLoader. Существует три способа регистрации расширений: декларативно, программно и автоматически.

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

  • Декларативная регистрация: аннотацию @ExtendWith (classReference.class) следует использовать для применения расширения к полям класса, тестовым интерфейсам, тестовым методам или пользовательским составным аннотациям.

// 1. Для тестового класса
@ExtendWith(LoggingExtension.class)
// 2. Составная аннотация
@ExtendWith({LoggingExtension.class, DivideExceptionHandler.class})
public class RegisteringExtensionTest {
    
    // 3. Для метода тестирования
    @ExtendWith(DivideExceptionHandler.class)
    @Test
    void divideTestMethod() {
        Calculate.divide(5, 0);
    }
    @ExtendWith(LoggingExtension.class)
    @Test
    void divideMethod() {
        Calculate.divide(0, 0);
    }
}

Для демонстрирации расширения JUnit 5, мы использовали пример, который показывает обработку исключений результатов теста:

public class DivideExceptionHandler implements TestExecutionExceptionHandler{
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable throwable) throws Throwable {
        // обработка исключения
        System.out.println("operation not allowed for division");
    }
}

Мы использовали аннотацию @ExtendWith (AdditionalOutputExtension.class) для регистрации указанного выше класса, чтобы среда JUnit могла использовать его на более позднем этапе.

@ExtendWith(AdditionalOutputExtension.class)
public class ArithmeticTest { 
    private int result = 5;
 
    @ExtendWith(DivideExceptionHandler.class)
    @Test
    void test_Divide_by_zero() {
      result = Calculate.divide(result, 0);
      System.out.println("test_Divide(5,0) => "+result);
    } 
}
  • Программная регистрация: мы можем использовать аннотацию @RegisterExtension, применив ее к полям в тестовых классах:

public class WebServerDemo {
    @RegisterExtension 
    static WebServerExtension server = WebServerExtension.builder()
                                        .enableSecurity(false) 
                                        .build();
 
    @Test 
    void getProductList() { 
    WebClient webClient = new WebClient();
    String serverUrl = server.getServerUrl();
     // Use WebClient to connect to web server using serverUrl and verify response 
    assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
     } 
}
  • Автоматическая регистрация: мы можем использовать java.util.ServiceLoader для автоматического обнаружения и регистрации сторонних расширений.

Условное выполнение теста JUnit 5 с аннотациями

Во-первых, условное выполнение теста позволяет запускать (включать) или пропускать (отключать) тестовые сценарии в зависимости от определенных условий использу\ API org.junit.jupiter.api.condition. Давайте посмотрим, как аннотации пакета условий можно использовать для реализации условного выполнения теста в JUnit 5.

1. Условия операционной системы

Условия операционной системы можно использовать с аннотациями @EnabledOnOs и @DisabledOnOs. Условия помогают запустить тест JUnit 5 на конкретной платформе (или операционной системе).

public class OsConditionalTest {
    
  @Test
  @EnabledOnOs(OS.MAC)
  void runOnlyOnMacOs() {
      System.out.println("Run the batch job only on MAC OS");
  }
  @Test
  @EnabledOnOs({ OS.LINUX, OS.MAC })
  void runOnlyOnLinuxOrMac() {
    System.out.println("Run the batch job only on LINUX or MAC OS");
  }
  @Test
  @DisabledOnOs(OS.WINDOWS)
  void notRunOnWindows() {
    System.out.println("Not run the batch job on WINDOWS OS");
  }
  
  @Test
  @EnabledOnOs({ OS.WINDOWS })
  void runOnlyOnWindows() {
    System.out.println("Run the batch job only on WINDOWS OS");
  }
  
  @Test
    @DisabledOnOs({OS.AIX, OS.LINUX, OS.SOLARIS})
    void notRunOnAIXorLinuxOrSolaris() {
        System.out.println("Not run the batch job on AIX or LINUX or SOLARIS");
    } 
}

2. Условия среды выполнения Java

Тестовые сценарии можно запускать при определенных условиях, связанных с JRE (Java Runtime Environment), или в определенном диапазоне диапазона версии JRE с использованием аннотаций @EnabledOnJre, @DisabledOnJre и @EnabledForJreRange.

public class JreConditionalTest {
    
      @Test
      @EnabledOnJre(JRE.JAVA_8)
      void runOnlyOnJava8() {
          System.out.println("Run the compatibility test only on JRE 8");
      }
      @Test
      @EnabledOnJre({JRE.JAVA_13, JRE.JAVA_14})
      void runOnlyOnJava13OrJava14() {
        System.out.println("Run the compatibility test only on JRE 13 and JRE 14");
      }
      @Test
      @DisabledOnJre(JRE.JAVA_13)
      void notRunOnJava13() {
        System.out.println("not run the compatibility test on JRE 13");
      }
      
      @Test
      @EnabledOnJre(JRE.JAVA_11)
      void runOnlyOnJava11() {
        System.out.println("Run the compatibility test only on JRE 11");
      }
      
      @Test
      @DisabledOnJre({JRE.JAVA_10, JRE.JAVA_11})
        void notRunOnJava10andJava11() {
        System.out.println("not Run the compatibility test on JRE 10 and JRE 11");
        }  
}

3. Условия собственности системы

Тестовые сценарии могут быть включены или отключены в зависимости от системного свойства с помощью аннотаций @EnabledIfSystemProperty и / или @DisabledIfSystemProperty.

public class SystemPropertyConditionalTest {
    @Disabled
    @Test
    void printSystemProperties() {
      //remove @Disabled to see System properties
      System.getProperties().forEach((key, value) -> System.out.println(key+" - "+value));
    }
    @Test
    @EnabledIfSystemProperty(named = "java.vm.vendor", matches = "Oracle.*")
    void runOnlyOnOracleJDK() {
      System.out.println("Run this only on Oracle JDK");
    }
    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*32.*")
    void runOnlyOn32bitOS() {
      System.out.println("Run this on only on 32 bit OS");
    }
    
    @Test
    @DisabledIfSystemProperty(named = "os.version", matches = ".*10.*")
    void notRunOnlyOnWindows10() {
      System.out.println("not run this only on windows 10 version");
    }
    
    @Test
    @EnabledIfSystemProperty(named = "os.version", matches = ".*10.*")
    void runOnlyOnWindows10() {
      System.out.println("Run this only on WINDOWS OS 10 version");
    }
}

4. Условия переменных окружающей среды

Тестовые сценарии JUnit 5 могут быть включены или отключены в зависимости от состояния (или значения) переменных среды. Это можно сделать с помощью аннотаций @EnabledIfEnvironmentVariable и @DisabledIfEnvironmentVariable в среде JUnit 5.

public class EnvironmentVariableConditionalTest {
    @Disabled
    @Test
    void printSystemProperties() {
      // Remove @Disabled to see environment properties
      System.getenv().forEach((key, value) -> System.out.println(key+" - "+value));
    }
    @Test
    @EnabledIfEnvironmentVariable(named = "COMPUTERNAME", matches = "sysname")
    void runOnlyOnPerticularMachine() {
      System.out.println("Run this only on particular server");
    }
    @Test
    @DisabledIfEnvironmentVariable(named = "PROCESSOR_ARCHITECTURE", matches = ".*32.*")
    void noRrunOn32bitOS() {
      System.out.println("Not run this on 32 bit OS");
    }
    
    @Test
    @EnabledIfEnvironmentVariable(named = "USERNAME", matches = "username")
    void runOnlyForParticularUser() {
      System.out.println("run this only for particular user in system");
    }
}

5. Пользовательские условия

Пользовательские условия могут быть установлены для включения или отключения тестовых сценарии через API расширения ExecutionCondition. Вот два способа реализации тестовых примеров, которые выполняются в определенных (настраиваемых) условиях:

  • Комбинация встроенных аннотаций для создания настраиваемой аннотации, которую можно будет использовать позже в качестве условия тестирования.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs({ OS.WINDOWS })
@EnabledIfSystemProperty(named = "os.version", matches = ".*10.*")
@EnabledIfEnvironmentVariable(named = "PROCESSOR_ARCHITECTURE", matches = ".*64.*")
public @interface RunOnlyOn64bitWindows10 {
 
}

Комбинированные встроенные аннотации внутри тестового условия могут использоваться как аннотации внутри тестового класса. Это поможет определить условия, при которых следует проводить тест.

public class CustomBuiltInTest {
    @RunOnlyOn64bitWindows10
    void runOnlyOn64bitWindows10() {
        System.out.println("Run only this on 64-bit Windows 10 System.");
      }
}

Пользовательские аннотации также могут быть созданы с нуля с помощью API расширения ExecutionCondition. Используя этот подход, вы можете обойтись без использования встроенных аннотаций. Чтобы продемонстрировать пользовательские аннотации с использованием примера расширения JUnit 5, мы запускаем тесты в условиях среды выполнения (т. е. среда может быть для разработки, контроля качества или производственная), как показано ниже:

public class EnvironmentConditionalTests {
    @Test
    @Environment(enabledFor = {"Dev", "QA"})
    void add() {
        Assertions.assertEquals(2, Calculate.add(1, 1));
    }
    @Test
    void multiply() {
        Assertions.assertEquals(6, Calculate.multiple(3, 2));
    }
}

Здесь условие для запуска теста add() выполняется в тестовой среде или среде разработки (не в реальном времени). Вот как вы можете создать аннотацию @Environment с нуля и реализовать ее в примере расширения JUnit 5:

  • Мы создаем файл Environment.java и устанавливаем атрибут enabledFor, чтобы добавить в него параметры. Затем созданная аннотация должна зарегистрировать расширение условия через файл EnvironmentExecutionCondition с помощью аннотации @ExtendWith.

  • Создайте файл EnvironmentExecutionCondition, в котором будут указаны все условия для реализации ExecutionCondition API.

@ExtendWith(EnvironmentExecutionCondition.class)
@Retention(RUNTIME)
public @interface Environment {
    String[] enabledFor();
}
public class EnvironmentExecutionCondition implements ExecutionCondition{
 
    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context){
        String activeEnvironment = System.getProperty("environment");
        if(activeEnvironment == null) {
            return ConditionEvaluationResult.disabled("There is no active environment");
        }
        Set<String> enabledEnvironments = getEnabledEnvironment(context);
        return enabledEnvironments.contains(activeEnvironment)
            ? ConditionEvaluationResult.enabled("active environment is enabled")
            : ConditionEvaluationResult.disabled("active environment is not enabled");
    }
 
    private Set<String> getEnabledEnvironment(ExtensionContext context) {
        Set<String> enabledEnvironments = new HashSet<>();
      context.getElement().ifPresent(element ->
 AnnotationSupport.findAnnotation(element, Environment.class)
                .map(Environment::enabledFor)
                .ifPresent(array -> enabledEnvironments.addAll(Arrays.asList(array)))
                );
        return enabledEnvironments;
    }       
}

При запуске тестов в среде Dev или QA, тест «add» будет активен и выполнен, тогда как тесты не будут выполняться, если вы находитесь в среде Prod.

Чтобы выполнить тесты в данной среде, запустите соответствующую команду для аргументов виртуальной машины в параметре «run configurations»:

  1. Среда разработки: -ea -Denvironment = Dev

  2. Среда обеспечения качества: -ea -Denvironment = QA

  3. Среда Prod (или Live): -ea -Denvironment = live

Прочтите: Как запустить тесты Junit из командной строки

Как создать расширения JUnit 5 путем реализации TestInstanceFactory

Мы можем создавать расширения JUnit 5, реализуя API TestInstanceFactory для создания экземпляров тестовых классов. Они должны выполняться перед выполнением каждого метода тестирования.

Затем созданный тестовый экземпляр можно получить из инфраструктуры внедрения зависимостей или путем вызова статического фабричного метода для его создания.

Следующий пример расширения JUnit 5 демонстрирует использование фабрик тестовых экземпляров во внешних и внутренних классах:

@ExtendWith(CustomTestInstanceFactory.class)
public class OuterTest {
    
    @Test
    void outer() {
    }


    @Nested
    // @ExtendWith(CustomTestInstanceFactory.class)
    class Inner {
 
        @Test
        void inner() {
        }
 
        @Nested
        // @ExtendWith(CustomTestInstanceFactory.class)
        class InnerInner {
 
            @Test
            void innerInner() {
            }
        }
    }
}
import static org.junit.platform.commons.util.ReflectionUtils.newInstance;
 
public class CustomTestInstanceFactory implements TestInstanceFactory{
 
    public Object createTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext extensionContext)
            throws TestInstantiationException {
    
        try {
            Optional<Object> outerInstance = factoryContext.getOuterInstance();
            Class<?> testClass = factoryContext.getTestClass();
            if (outerInstance.isPresent()) {
                System.out.println("createTestInstance() called for inner class: " 
                                     + testClass.getSimpleName());
                 return newInstance(testClass, outerInstance.get());
            }
            else {
                System.out.println("createTestInstance() called for outer class: "
                                      + testClass.getSimpleName());
                 return newInstance(testClass);
            }
        }
        catch (Exception e) {
            throw new TestInstantiationException(e.getMessage(), e);
        }
        
    }
}

Как протестировать обратные вызовы жизненного цикла в JUnit 5

Обратные вызовы жизненного цикла - это функции, которые автоматически выполняются до или после определенных методов модели. Например, вы можете использовать обратные вызовы жизненного цикла для автоматического вычисления значения атрибута «full name» перед созданием или обновлением записи пользователя.

Методы жизненного цикла и жизненный цикл тестового экземпляра

В жизненном цикле основного тестового экземпляра JUnit 5 определяет жизненный цикл класса и метода, управляемый следующими аннотациями:

  1. @BeforeAll

  2. @BeforeEach

  3. @AfterEach

  4. @AfterAll

Методы, помеченные @BefсегоoreAll и @AfterAll, должны выполняться до и после всех тестовых методов в классе. С другой стороны, методы, аннотированные @BeforeEach и @AfterEach, должны выполняться соответственно до и после каждого метода тестирования.

JUnit создает новый экземпляр для тестового класса перед запуском каждого теста в жизненном цикле тестового экземпляра. Такое поведение направлено на запуск каждого теста отдельно и, таким образом, позволяет избежать побочных эффектов от запуска других тестов.

class TestInstanceLifecycle {
 
    public TestInstanceLifecycle() {
        super();
        System.out.println("test instance Constructor");
    }
 
    @BeforeAll
    static void setUpBeforeClass() throws Exception {
        System.out.println("@BeforeAll : Before the entire test fixture");
    }
 
    @AfterAll
    static void tearDownAfterClass() throws Exception {
         System.out.println("@AfterAll : After the entire test fixture");
    }
    @BeforeEach
    void setUp() throws Exception {
         System.out.println("@BeforeEach : Before each test");
    }
    @AfterEach
    void tearDown() throws Exception {
        System.out.println("@AfterEach : After each test");
    }
    @Test
    void firstTest() {
        System.out.println("First test");
    }   
    @Test
    void secondTest() {
        System.out.println("Second test");
    }
 
}

Выполнение этого кода дает следующий результат:

@BeforeAll: Before the entire test fixture
test instance Constructor
   @BeforeEach: Before each test
	First test
   @AfterEach: After each test
            test instance Constructor
    @BeforeEach: Before each test
	Second test
    @AfterEach: After each test
@AfterAll: After the entire test fixture

Исходя из результата выполнения теста, поведение по умолчанию - следующее:

Жизненный цикл каждого метода

Поведение жизненного цикла теста можно изменить с помощью API org.junit.jupiter.api.TestInstance, который позволяет изменить жизненный цикл по умолчанию (для тестового класса или метода тестирования). Это можно сделать, добавив в тестовый класс аннотацию @TestInstance (TestInstance.Lifecycle.PER_CLASS).

Вот обновленный результат выполнения после модификации поведения по умолчанию (жизненного цикла теста):

test instance Constructor
@BeforeAll: Before the entire test fixture
@BeforeEach: Before each test
	First test
@AfterEach: After each test
@BeforeEach: Before each test
	Second test
@AfterEach: After each test
@AfterAll: After the entire test fixture

По результату выполнения теста измененное поведение следующее:

Жизненный цикл для каждого класса

Жизненный цикл расширения JUnit 5

Помимо жизненного цикла для каждого класса и для каждого метода, JUnit 5 Jupiter предлагает различные интерфейсы, которые определяют API-интерфейсы для расширения тестов в различных точках жизненного цикла выполнения. Поэтому JUnit 5 вызывает обратные вызовы расширений для реализации требуемого поведения.

API-интерфейсы являются частью пакета org.junit.jupiter.api.extension. Вот API, определяющие жизненный цикл расширения:

  • AfterAllCallback

  • AfterEachCallback

  • BeforeAllCallback

  • BeforeEachCallback

Мы можем создать расширение для тестового класса, реализовав интерфейсы BeforeAllCallback, AfterAllCallback, BeforeEachCallback и AfterEachCallback.

public class ExtensionCallbackLifecycle implements BeforeAllCallback, AfterAllCallback, 
BeforeEachCallback, AfterEachCallback {
 
    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        System.out.println("After Each from AfterEachCallback Extension");
    }
 
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        System.out.println("Before Each from BeforeEachCallback Extension");
    }
 
    @Override
    public void afterAll(ExtensionContext context) throws Exception {
        System.out.println("After All from AfterAllCallback Extension");
    }
 
    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        System.out.println("Before All from BeforeAllCallback Extension");
    }
 
}

Вот как применить указанную точку расширения к тест-классу:

@ExtendWith(ExtensionCallbackLifecycle.class)
public class ExtensionLifecycleTest {
 
    public ExtensionLifecycleTest() {
            super();
           System.out.println("Test instance constructor");
    }
    @BeforeEach
    void beforeEachTest() {
        System.out.println("Before each test");
     }
 
    @AfterEach
    void afterEachTest() {
        System.out.println("After each test");
     }
 
    @Test
    void firstTest() {
        System.out.println("First test");
    }
 
    @Test
    void secondTest() {
        System.out.println("Second test");
    }
}

Вот результат выполнения:

Before All from BeforeAllCallback Extension
Test instance constructor
Before Each from BeforeEachCallback Extension
Before each test
First test
After each test
After Each from AfterEachCallback Extension
Test instance constructor
Before Each from BeforeEachCallback Extension
Before each test
Second test
After each test
After Each from AfterEachCallback Extension
After All, from AfterAllCallback Extension

Постобработка тестового экземпляра в JUnit 5

Модель расширений Juniper обеспечивает возможность постобработки тестовых экземпляров после создания тестовых экземпляров путем реализации интерфейса TestInstancePostProcessor. В соответствии с фабрикой тестового экземпляра он может вызывать метод инициализации в тестовом экземпляре, используя, например, зависимости внедрения в экземпляр для использования постобработки тестового экземпляра.

Чтобы проиллюстрировать это, мы возьмем пример системы журналирования из API-интерфейса log4j, который выполняет и записывает журналы после каждого выполнения теста. Давайте рассмотрим пример исключения JUnit 5:

public class LoggingPostProcessExtension implements TestInstancePostProcessor{
 
  @Override
  public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
        Logger logger = LogManager.getLogger(testInstance.getClass()
                                                         .getName());
System.out.println("Test instance Post-          Process Extension called on :"+ testInstance.getClass().getName());
        testInstance.getClass()
                    .getMethod("createLogger", Logger.class)
                    .invoke(testInstance, logger); 
    }   
}
public class ArithmeticTest {
    private int result = 5;


    @ExtendWith(LoggingPostProcessExtension.class)
    @Test
    void test_Divide() {
      result = Calculate.divide(result, 5);
      System.out.println("test_Divide(5,5) => "+ result);
      Assertions.assertEquals(1, result);
    }   

Обратный вызов перед уничтожением тестового экземпляра в JUnit 5

Модель расширений также определяет API для расширений, которые необходимо обработать между тестовыми экземплярами и их окончательным уничтожением. Например, обратный вызов перед уничтожением тестового экземпляра обычно используется в таких случаях, как очистка внедренных зависимостей после их использования в тестовом экземпляре.

public class DisplayPredestroyedInstances implements  TestInstancePreDestroyCallback{
 
    @Override
    public void preDestroyTestInstance(ExtensionContext ctx) throws Exception {
        
          List<Object> destroyedInstances =
                    new ArrayList<>(context.getRequiredTestInstances().getAllInstances());
            for (Optional<ExtensionContext> current = context.getParent(); 
                    current.isPresent(); 
                    current = current.get().getParent()) {
                current.get().getTestInstances()
                             .map(TestInstances::getAllInstances)
                             .ifPresent(destroyedInstances::removeAll);
            }
            Collections.reverse(destroyedInstances);
            destroyedInstances.forEach(testInstance -> 
            System.out.println("preDestroy: " + testInstance));
    }
}
public class ArithmeticTest {
    
    private int result = 5;
      @ExtendWith(DisplayPredestroyedInstances.class)
      @Test
      void test_Multiply() {
        result = Calculate.multiple(result, 5);
        System.out.println("test_Multiply(5,5) => "+ result);
        Assertions.assertEquals(25, result);
      }
}

Разрешение параметра в JUnit 5

Большинство методов тестирования не имеют параметров. Мы используем интерфейс ParameterResolver при использовании параметров, который определяет API org.junit.jupiter.api.extension.ParameterResolver для расширений. Он предоставляет функциональные возможности для динамического разрешения параметров во время выполнения.

Следующие конструкторы и аннотированные методы тестового класса могут иметь один или несколько параметров:

  1. @Test

  2. @TestFactory

  3. @BeforeEach

  4. @AfterEach

  5. @BeforeAll

  6. @AfterAll

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

Эти параметры должны быть разрешены во время выполнения экземпляром типа ParameterResolver, который необходимо зарегистрировать ранее.

По умолчанию JUnit 5 автоматически регистрирует ParameterResolver, используя три встроенных преобразователя:

  • TestInfoParameterResolver: используется для разрешения, внедрения экземпляра типа TestInfo и получения информации о тесте, который выполняется.

  • RepetitionInfoParameterResolver: используется для внедрения экземпляра типа RepetitionInfo только для повторных тестов.

  • TestReporterParameterResolver: используется для внедрения экземпляра типа TestReporter, позволяя ему добавлять полезную информацию в отчет о тестировании.

Если вы используете JUnit 4, вы можете ознакомиться с блогом, в котором подробно рассказывается о параметризации в JUnit для Selenium Automation.

public class BuiltInParamResolver {
    @Test
    @DisplayName("TestInfo Param Resolver")
    void firstTestCase(TestInfo testInfo) {
        assertEquals("TestInfo Param Resolver", testInfo.getDisplayName());
        System.out.println("TestInfo executed !");
    }
    
     @RepeatedTest(3)
     @DisplayName("RepetitionInfo Param Resolver")
      void test_repeted(RepetitionInfo repetitionInfo) {
        System.out.println("start test_repeted : "+repetitionInfo.getCurrentRepetition());
        assertEquals(9, Calculate.add(5, 4));
      }
    
    @Test
    @DisplayName("Testreport Param Resolver")
    void testReport(TestReporter testReporter) {
       testReporter.publishEntry("test reporter with single value : "+Calculate.add(4, 3));
        assertEquals(7, Calculate.add(4, 3));
    }
}

Обработка исключений в JUnit 5

Интерфейс TestExecutionExceptionHandler определяет API, реализующий расширения, позволяющие полностью настроить поведение тестового примера при возникновении исключения.

В продолжение предыдущего примера расширения JUnit 5 мы использовали ArithmeticException для создания тестового класса в тестовом сценарии для divide, как показано ниже:

public class ArithmeticTest {
    
    private int result = 5;
 
    @ExtendWith(DivideExceptionHandler.class)
    @Test
    void test_Divide_by_zero() {
      result = Calculate.divide(result, 0);
      System.out.println("test_Divide(5,0) => "+ result);
    } 
}

Он расширен до класса обработчика исключений для обработки исключения, вызванного операцией деления (при обработке деления на ноль):

public class DivideExceptionHandler implements TestExecutionExceptionHandler{
 
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable throwable) 
throws Throwable {
         // handle exception 
        System.out.println("operation not allowed for division");
    }
}

Можно использовать традиционный метод создания исключения (с помощью try… catch, Rules и т. д.) или через аннотации, реализовав интерфейс TestExecutionExceptionHandler.

Читать дополнительно: Mastering Selenium Testing With JUnit Asserts

Сторонние расширения фреймворка в JUnit 5

Принцип JUnit - предоставить легко расширяемую базовую структуру, которая позволяет пользователям работать быстрее, чем разработчики API. Эта функция позволяет создавать API-интерфейсы, которые служат основой для сторонних библиотек.

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

  • MockitoExtension

  • Selenium-Jupiter

  • Spring TestContext: SpringExtension for Jupiter

1. MockitoExtension

JUnit 5 лучше всего подходит для запуска модульных тестов. Однако при выполнении интеграционного тестирования между модулями (или взаимозависимыми ресурсами) и проверки взаимодействия заглушки или макеты используются для имитации (или представления) зависимых или недоступных ресурсов. Mockito - это фреймворк, который позволяет создавать фиктивные объекты для интеграционного тестирования.

Вот основные способы использования MockitoExtension:

  1. Ручной подход

  2. Использование аннотаций

  3. Использование расширений JUnit 5, доступных в артефакте mockito-junit-jupiter (наиболее предпочтительный вариант)

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>2.23.4</version>
    <scope>test</scope>
</dependency>

Использование расширения Mockito можно увидеть в действии, используя расширение и добавив @ExtendWith в тестовый класс и аннотируя моделируемые поля с помощью @Mock.

Например, если нам нужно протестировать класс SERVICE и имитировать базу данных, нам нужно использовать следующий код:

public class Database {
    public boolean isAvailable() {
        // TODO implement the access to the database
        return false;
    }
    public int getUniqueId() {
        return 42;
    }
}
public class Service {
        private Database database;
        public Service(Database database) {
            this.database = database;
        }
        public boolean query(String query) {
            return database.isAvailable();
        }
        @Override
        public String toString() {
            return "Using database with id: " + String.valueOf(database.getUniqueId());
        }
}

Тестовый класс будет выглядеть так:

@ExtendWith(MockitoExtension.class)
public class ServiceTest {
        @Mock
        Database databaseMock;                                  
 
        @Test
        public void testQuery() {
            assertNotNull(databaseMock);
            when(databaseMock.isAvailable())
     .thenReturn(true);  
            Service t = new Service(databaseMock);             
            boolean check = t.query("* from t");                
            assertTrue(check);
        }
}

2. Selenium-Jupiter

Объединив силу Selenium, самой популярной среды тестирования веб-браузеров, и мощность JUnit 5, selenium-jupiter позволяет создавать тесты Selenium с использованием локальных и/или удаленных браузеров. Благодаря этому вы можете запускать различные типы тестов для проверки функциональности веб-приложений и мобильных приложений. Кроме того, расширение selenium-jupiter может использоваться для автоматизации Selenium тестирования.

Выполните автоматизацию Selenium тестирования в облаке с помощью JUnit Framework.

Для проектов Maven следует использовать следующую зависимость:

<dependency>
	<!-- https://mvnrepository.com/artifact/io.github.bonigarcia/selenium-jupiter -->
	<dependency>
	    <groupId>io.github.bonigarcia</groupId>
	    <artifactId>selenium-jupiter</artifactId>
	    <version>3.4.0</version>
</dependency>

Selenium-Jupiter можно использовать, просто используя аннотацию @ExtendWith в интерфейсе SeleniumJupiter для выполнения тестирования кроссбраузерной совместимости. Вот демонстрационный пример:

@ExtendWith(SeleniumJupiter.class)
public class CrossBrowserTest {
    @Test
    void testWithOneChrome(ChromeDriver chromeDriver) {
        // Use Chrome in this test
        chromeDriver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(chromeDriver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }
 
    @Test
    void testWithFirefox(FirefoxDriver firefoxDriver) {
        // Use Firefox in this test
        firefoxDriver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(firefoxDriver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }
 
    @Test
    void testWithChromeAndFirefox(ChromeDriver chromeDriver,
            FirefoxDriver firefoxDriver) {
        // Use Chrome and Firefox in this test
        chromeDriver.get("http://www.seleniumhq.org/");
        firefoxDriver.get("http://junit.org/junit5/");
        Assertions.assertEquals(chromeDriver.getTitle(), "SeleniumHQ Browser Automation");
        Assertions.assertEquals(firefoxDriver.getTitle(), "JUnit 5");
    }
}

Читать далее: Automated Testing With JUnit And Selenium For Browser Compatibility

Как использовать Selenium-Jupiter для автоматизации Selenium тестирования

Selenium-Jupiter поддерживает тестирование удаленных веб-браузеров в Selenium Grid с помощью комбинации DriverCapabilities и RemoteWebDriver. Вы также можете выполнить параллельное тестирование в Selenium, запустив тесты в различных комбинациях браузера и платформы с помощью LambdaTest.

@ExtendWith(SeleniumJupiter.class)
public class RemoteBrowserJupiterTest<WebDriver> {
    @DriverUrl
    String url = "http://localhost:4444/wd/hub";
 
    @BeforeAll
    static void setup() throws Exception {
        // Start hub
        GridLauncherV3.main(new String[] { "-role", "hub", "-port", "4444" });
 
        // Register Chrome in hub
        WebDriverManager.chromedriver().setup();
        GridLauncherV3.main(new String[] { "-role", "node", "-hub",
                "http://localhost:4444/grid/register", "-browser",
                "browserName=chrome", "-port", "5555" });
 
        // Register Firefox in hub
        WebDriverManager.firefoxdriver().setup();
        GridLauncherV3.main(new String[] { "-role", "node", "-hub",
                "http://localhost:4444/grid/register", "-browser",
                "browserName=firefox", "-port", "5556" });
    }
    @Test
    void testWithRemoteChrome(
            @DriverUrl("http://localhost:4444/wd/hub")
            @DriverCapabilities("browserName=chrome") RemoteWebDriver driver) {
        exercise(driver);
    }
 
    @Test
    void testWithRemoteFirefox(
                @DriverUrl("http://localhost:4444/wd/hub")
                @DriverCapabilities("browserName=firefox") RemoteWebDriver driver) {
        exercise(driver);
    }
 
    void exercise(WebDriver driver) {
        driver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(driver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }

Как использовать Selenium-Jupiter для тестирования мобильных устройств

Чтобы создать экземпляр ApiumDriver для управления мобильными устройствами, аннотация DriverCapabilities. Selenium-Jupiter автоматически запустит экземпляр сервера Appium.

@ExtendWith(SeleniumJupiter.class)
public class AppiumJupiterTest {
        @DriverUrl
        String url = "http://localhost:4723/wd/hub";
 
        @DriverCapabilities
        DesiredCapabilities capabilities = new DesiredCapabilities();
        {
            capabilities.setCapability("browserName", "chrome");
            capabilities.setCapability("deviceName", "Samsung Galaxy S6");
        }
 
        @Test
        void testWithAndroid(AppiumDriver<WebElement> driver) {
            driver.get("https://bonigarcia.github.io/selenium-jupiter/");
            Assertions.assertEquals(driver.getTitle(),
                    "JUnit 5 extension for Selenium");
        }
}

Как использовать Selenium-Jupiter для выполнения автоматизации Selenium тестирования в Cloud Grid

Selenium-Jupiter позволяет запускать автоматизации Selenium тестов на облачной платформе кроссбраузерного тестирования, такой как LambdaTest. Основными преимуществами облачного тестирования являются улучшенное покрытие браузера, устранение связанных с окружающей средой задержек в расписании, повышение качества продукта и снижение совокупной стоимости владения (TCO). Ознакомьтесь с нашим руководством по облачному тестированию, в котором описаны многочисленные преимущества переноса тестов в облачную среду Selenium Grid, такую ​​как LambdaTest.

После создания учетной записи на LamdaTest обратите внимание на имя пользователя и доступ из раздела профиля LambdaTest. Эти учетные данные необходимы для доступа к облачной сетке. Затем вы можете сгенерировать желаемые возможности с помощью LambdaTest Capabilities Generator.

Ниже показан пример запуска теста JUnit 5 в LambdaTest Grid:

@ExtendWith(SeleniumJupiter.class)
public class LambdaTestSeleniumJupiter {
    public RemoteWebDriver driver = null;
    String username = "mukendik";
    String accessKey = "mP7l3gCMXcLmwy7alMb6rAuqAOKcAAXMCklWlHLWbi8XhY0JWd";
 
    {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platform", "Windows 7"); // MacOS Catalina Windows 10   
        capabilities.setCapability("browserName", "Chrome");
        capabilities.setCapability("version", "91.0"); // If this cap isn't specified, it will just get the any available one
        capabilities.setCapability("resolution","1024x768");
        capabilities.setCapability("build", "Selenium jupiter");
        capabilities.setCapability("name", "LambdaTest selenium jupiter");
        capabilities.setCapability("network", true); // To enable network logs
        capabilities.setCapability("visual", true); // To enable step by step screenshot
        capabilities.setCapability("video", true); // To enable video recording
        capabilities.setCapability("console", true); // To capture console logs
    
        try {       
            driver= new RemoteWebDriver(new URL("https://"+username+":"+accessKey+
                                  "@hub.lambdatest.com/wd/hub"), capabilities);            
        } catch (MalformedURLException e) {
            System.out.println("Invalid grid URL");
        }
    }
    @Test
    public void testWithLambdaTest() throws Exception {
                try {
                    driver.get("https://lambdatest.github.io/sample-todo-app/");
                    driver.findElement(By.name("li1")).click();
                    driver.findElement(By.name("li2")).click();
                    driver.findElement(By.id("sampletodotext")).clear();
                    driver.findElement(By.id("sampletodotext"))
                          .sendKeys("Hey, Let's add it to list");
                    driver.findElement(By.id("addbutton")).click();
                    driver.quit();                  
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
    }
}

Вот снимок во время выполнения, который указывает, что выполнение теста было успешным.

3. Spring TestContext: SpringExtension для Jupiter

Spring TestContext, представленный в Spring 5, представляет собой среду Spring, которая предлагает полную интеграцию с моделью программирования JUnit 5 Jupiter. Его можно найти в пакете org.springframework.test.context.junit.jupiter.SpringExtension.

Его можно использовать, просто аннотируя тестовый класс JUnit Jupiter любой из следующих аннотаций:

  1. @ExtendWith(SpringExtension.class)

  2. @SpringJunitConfig(TestConfig.class)

  3. @SpringJUnitWebConfig(TestConfig.class)

Ниже показан пример расширения JUnit 5, демонстрирующий использование Spring TestContext:

//Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
//Instructs Spring to load an ApplicationContext from AppConfig.class
@ContextConfiguration(classes = AppConfig.class)
public class SpringExtensionTest {
    
    @Autowired
    private MyService myService;
 
    @BeforeAll
    static void initAll() {
        System.out.println("---Inside initAll---");
    }
 
    @BeforeEach
    void init(TestInfo testInfo) {
        System.out.println("Start..." + testInfo.getDisplayName());
    }
 
    @Test
    public void messageTest() {
        String msg = myService.getMessage();
        assertEquals("Hello World!", msg);
    }
 
    @Test
    public void multiplyNumTest() {
        int val = myService.multiplyNum(5, 10);
        assertEquals(50, val);
    }
 
    @Test
    public void idAvailabilityTest() {
        boolean val = myService.isIdAvailable(100);
        Assertions.assertTrue(val);
    }
 
    @AfterEach
    void tearDown(TestInfo testInfo) {
        System.out.println("Finished..." + testInfo.getDisplayName());
    }
 
    @AfterAll
    static void tearDownAll() {
        System.out.println("---Inside tearDownAll---");
    }
 
}

@Configuration
@ComponentScan("com.concretepage")
public class AppConfig {
 
}

@Service
public class MyService {
 
    public String getMessage() {
        return "Hello World!";
    }
    
     public int multiplyNum(int num1, int num2) {
            return num1 * num2;
        }
     
    public boolean isIdAvailable(long id) {
        if (id == 100) {
            return true;
        }
        return false;
    }    
}

public class SpringProfileDemo {
 
    public static void main(String[] args) {
        
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.scan("com.concretepage");
        ctx.refresh();
        MyService myService = ctx.getBean(MyService.class);
        System.out.println(myService.getMessage()); 
 
    }
}

Заключение и рекомендации

Модель расширения JUnit 5, встроенная в Jupiter, решила внутренние проблемы в точках расширения JUnit 4. Модель реализует несколько встроенных точек расширения и обеспечивает их настройку и групповое использование. Это позволяет разработчикам расширений реализовывать интерфейсы для включения дополнительных возможностей для JUnit 5.

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

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

Мы также можем более подробно проработать те, которые не кажутся вам понятными в этом руководстве. Нам также интересны ваши отзывы об использовании точек расширения JUnit Jupiter в ваших проектах. Исходный код приведенных выше примеров можно найти на GitHub.