Перейти к основному содержимому

Написание шаблонов для тестовых случаев с использованием JUnit 5

· 7 мин. чтения

1. Обзор

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

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

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

Начнем с добавления зависимостей в наш pom.xml .

Нам нужно добавить основную зависимость JUnit 5 junit-jupiter-engine :

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

В дополнение к этому нам также нужно добавить зависимость junit-jupiter-api :

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
</dependency>

Точно так же мы можем добавить необходимые зависимости в наш файл build.gradle :

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.1'

3. Постановка задачи

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

Давайте предположим, что теперь мы хотим запускать наш тестовый метод несколько раз — не только с разными параметрами, но и каждый раз в другом контексте вызова.

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

  • по разным параметрам
  • подготовка экземпляра тестового класса по-другому, то есть внедрение разных зависимостей в тестовый экземпляр
  • запуск теста в различных условиях, например включение/отключение подмножества вызовов, если среда « QA »
  • работа с другим поведением обратного вызова жизненного цикла — возможно, мы хотим настроить и отключить базу данных до и после подмножества вызовов

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

4. Тестовые шаблоны

Сами тестовые шаблоны не являются тестовыми наборами. Вместо этого, как следует из их названия, они являются просто шаблонами для заданных тестовых случаев. Они представляют собой мощное обобщение параметризованных и повторяющихся тестов.

Тестовые шаблоны вызываются один раз для каждого контекста вызова, предоставленного им провайдером(ами) контекста вызова.

Давайте теперь посмотрим на пример тестовых шаблонов. Как мы установили выше, основными действующими лицами являются:

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

4.1. Метод тестовой цели

В этом примере мы собираемся использовать простой метод UserIdGeneratorImpl.generate в качестве нашей тестовой цели.

Давайте определим класс UserIdGeneratorImpl :

public class UserIdGeneratorImpl implements UserIdGenerator {
private boolean isFeatureEnabled;

public UserIdGeneratorImpl(boolean isFeatureEnabled) {
this.isFeatureEnabled = isFeatureEnabled;
}

public String generate(String firstName, String lastName) {
String initialAndLastName = firstName.substring(0, 1).concat(lastName);
return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName;
}
}

Метод generate , который является нашей тестовой целью, принимает в качестве параметров firstName и lastName и генерирует идентификатор пользователя. Формат идентификатора пользователя зависит от того, включен ли переключатель функций.

Давайте посмотрим, как это выглядит:

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned
Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

Далее давайте напишем метод тестового шаблона.

4.2. Метод тестового шаблона

Вот тестовый шаблон для нашего тестового целевого метода UserIdGeneratorImpl.generate :

public class UserIdGeneratorImplUnitTest {
@TestTemplate
@ExtendWith(UserIdGeneratorTestInvocationContextProvider.class)
public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) {
UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled());

String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName());

assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId());
}
}

Рассмотрим подробнее метод тестового шаблона.

Для начала создадим наш метод тестового шаблона, пометив его аннотацией JUnit 5 @TestTemplate .

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

Кроме того, метод шаблона получает в качестве параметра экземпляр UserIdGeneratorTestCase . Это просто класс-оболочка для входных данных и ожидаемого результата теста:

public class UserIdGeneratorTestCase {
private boolean isFeatureEnabled;
private String firstName;
private String lastName;
private String expectedUserId;

// Standard setters and getters
}

Наконец, мы вызываем целевой метод теста и утверждаем, что результат соответствует ожидаемому.

Теперь пришло время определить нашего поставщика контекста вызова .

4.3. Поставщик контекста вызова

Нам нужно зарегистрировать хотя бы один TestTemplateInvocationContextProvider с нашим тестовым шаблоном. Каждый `` зарегистрированный TestTemplateInvocationContextProvider предоставляет поток экземпляров TestTemplateInvocationContext .

Ранее, используя аннотацию @ExtendWith , мы зарегистрировали UserIdGeneratorTestInvocationContextProvider в качестве нашего провайдера вызова.

Давайте определим этот класс сейчас:

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider {
//...
}

Наш контекст вызова реализует интерфейс TestTemplateInvocationContextProvider , который имеет два метода:

  • поддерживаетTestTemplate
  • обеспечитьTestTemplateInvocationContexts

Начнем с реализации метода supportsTestTemplate :

@Override
public boolean supportsTestTemplate(ExtensionContext extensionContext) {
return true;
}

Механизм выполнения JUnit 5 сначала вызывает метод supportsTestTemplate , чтобы проверить, применим ли поставщик для данного ExecutionContext . В этом случае мы просто возвращаем true .

Теперь давайте реализуем метод ProvideTestTemplateInvocationContexts :

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext extensionContext) {
boolean featureDisabled = false;
boolean featureEnabled = true;

return Stream.of(
featureDisabledContext(
new UserIdGeneratorTestCase(
"Given feature switch disabled When user name is John Smith Then generated userid is JSmith",
featureDisabled,
"John",
"Smith",
"JSmith")),
featureEnabledContext(
new UserIdGeneratorTestCase(
"Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith",
featureEnabled,
"John",
"Smith",
"baelJSmith"))
);
}

Целью метода providerTestTemplateInvocationContexts является предоставление потока экземпляров TestTemplateInvocationContext . В нашем примере он возвращает два экземпляра, предоставленные методами featureDisabledContext и featureEnabledContext . Следовательно, наш тестовый шаблон запустится дважды.

Далее рассмотрим два экземпляра TestTemplateInvocationContext , возвращаемые этими методами.

4.4. Экземпляры контекста вызова

Контексты вызова являются реализациями интерфейса TestTemplateInvocationContext и реализуют следующие методы:

  • getDisplayName — укажите тестовое отображаемое имя
  • getAdditionalExtensions — возвращает дополнительные расширения для контекста вызова

Давайте определим метод featureDisabledContext , который возвращает наш первый экземпляр контекста вызова:

private TestTemplateInvocationContext featureDisabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return userIdGeneratorTestCase.getDisplayName();
}

@Override
public List<Extension> getAdditionalExtensions() {
return asList(
new GenericTypedParameterResolver(userIdGeneratorTestCase),
new BeforeTestExecutionCallback() {
@Override
public void beforeTestExecution(ExtensionContext extensionContext) {
System.out.println("BeforeTestExecutionCallback:Disabled context");
}
},
new AfterTestExecutionCallback() {
@Override
public void afterTestExecution(ExtensionContext extensionContext) {
System.out.println("AfterTestExecutionCallback:Disabled context");
}
}
);
}
};
}

Во-первых, для контекста вызова, возвращаемого методом featureDisabledContext , мы регистрируем следующие расширения :

  • GenericTypedParameterResolver — расширение преобразователя параметров
  • BeforeTestExecutionCallback — расширение обратного вызова жизненного цикла, которое запускается непосредственно перед выполнением теста.
  • AfterTestExecutionCallback — расширение обратного вызова жизненного цикла, которое запускается сразу после выполнения теста.

Однако для второго контекста вызова, возвращаемого методом featureEnabledContext , зарегистрируем другой набор расширений (сохранив GenericTypedParameterResolver ):

private TestTemplateInvocationContext featureEnabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return userIdGeneratorTestCase.getDisplayName();
}

@Override
public List<Extension> getAdditionalExtensions() {
return asList(
new GenericTypedParameterResolver(userIdGeneratorTestCase),
new DisabledOnQAEnvironmentExtension(),
new BeforeEachCallback() {
@Override
public void beforeEach(ExtensionContext extensionContext) {
System.out.println("BeforeEachCallback:Enabled context");
}
},
new AfterEachCallback() {
@Override
public void afterEach(ExtensionContext extensionContext) {
System.out.println("AfterEachCallback:Enabled context");
}
}
);
}
};
}

Для второго контекста вызова мы регистрируем следующие расширения:

  • GenericTypedParameterResolver — расширение преобразователя параметров
  • DisabledOnQAEnvironmentExtension — условие выполнения для отключения теста, если свойство среды (загруженное из файла application.properties ) равно « qa » .
  • BeforeEachCallback — расширение обратного вызова жизненного цикла, которое запускается перед выполнением каждого тестового метода.
  • AfterEachCallback — расширение обратного вызова жизненного цикла, которое запускается после выполнения каждого тестового метода.

Из вышеприведенного примера ясно видно, что:

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

В результате тестовый метод может вызываться несколько раз в совершенно другом контексте вызова каждый раз. И, зарегистрировав несколько поставщиков контекста, мы можем предоставить еще больше дополнительных уровней контекстов вызова, в которых можно запускать тест.

5. Вывод

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

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

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

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