Полное руководство по расширениям JUnit 5
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:
Платформа JUnit: предоставляет API для инструментов обнаружения и запуска тестов. Он определяет интерфейс между JUnit и клиентами, которые хотят запускать тесты из IDE, инструментов сборки или консоли.
JUnit Jupiter: предоставляет API на основе аннотаций для написания модульных тестов JUnit 5, а также механизм тестирования, который позволяет их запускать.
JUnit Vintage: предлагает механизм тестирования для запуска тестов JUnit 3 и JUnit 4, тем самым обеспечивая обратную совместимость (с более ранними версиями платформы JUnit).
Цель этой архитектуры - разделить обязанности по тестированию, выполнению и расширению. Это также облегчает интеграцию других тестовых фреймворков с фреймворком JUnit.
Модель программирования или Модель расширения
Если вы QA-инженер, который регулярно пишет тесты, вы обязательно будете использовать модель программирования. С другой стороны, модель расширения предоставляет несколько интерфейсов в виде API-интерфейсов расширений, которые могут быть реализованы поставщиками расширений (разработчиками или поставщиками инструментов) для расширения основных функций 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»:
Среда разработки: -ea -Denvironment = Dev
Среда обеспечения качества: -ea -Denvironment = QA
Среда 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 определяет жизненный цикл класса и метода, управляемый следующими аннотациями:
@BeforeAll
@BeforeEach
@AfterEach
@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 для расширений. Он предоставляет функциональные возможности для динамического разрешения параметров во время выполнения.
Следующие конструкторы и аннотированные методы тестового класса могут иметь один или несколько параметров:
@Test
@TestFactory
@BeforeEach
@AfterEach
@BeforeAll
@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:
Ручной подход
Использование аннотаций
Использование расширений 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 любой из следующих аннотаций:
@ExtendWith(SpringExtension.class)
@SpringJunitConfig(TestConfig.class)
@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.