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

Внедрение параметров в модульные тесты JUnit Jupiter

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

1. Обзор

До JUnit 5, чтобы представить классную новую функцию, команда JUnit должна была сделать это с основным API. С JUnit 5 команда решила, что пришло время расширить возможности основного API JUnit за пределы самого JUnit, основная философия JUnit 5 называется « предпочитать точки расширения функциям ».

В этой статье мы сосредоточимся на одном из таких интерфейсов точек расширения — ParameterResolver , который можно использовать для ввода параметров в методы тестирования. Существует несколько различных способов сообщить платформе JUnit о вашем расширении (процесс, известный как «регистрация»), и в этой статье мы сосредоточимся на декларативной регистрации (т. е. регистрации через исходный код).

2. Разрешитель параметров

Внедрение параметров в ваши методы тестирования можно было выполнить с помощью JUnit 4 API, но это было довольно ограничено. С помощью JUnit 5 API Jupiter можно расширить — за счет реализации ParameterResolver — для обслуживания объектов любого типа в ваших методах тестирования. Давайте посмотрим.

2.1. FooParameterResolver

public class FooParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Foo.class;
}

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

Во-первых, нам нужно реализовать ParameterResolver , который имеет два метода:

  • supportsParameter() – возвращает true, если тип параметра поддерживается (в данном примере Foo), и
  • resolveParamater() — обслуживает объект правильного типа (в этом примере — новый экземпляр Foo), который затем будет внедрен в ваш тестовый метод.

2.2. Футест

@ExtendWith(FooParameterResolver.class)
public class FooTest {
@Test
public void testIt(Foo fooInstance) {
// TEST CODE GOES HERE
}
}

Затем, чтобы использовать расширение, нам нужно объявить его, т. е. сообщить о нем платформе JUnit, через аннотацию @ExtendWith (строка 1).

Когда платформа JUnit запускает ваш модульный тест, она получает экземпляр Foo из FooParameterResolver и передает его методу testIt() (строка 4).

Расширение имеет область влияния , которая активирует расширение в зависимости от того, где оно объявлено.

Расширение может быть активным в:

  • уровень метода, где он активен только для этого метода, или
  • уровень класса, где он активен для всего тестового класса, или тестовый класс @Nested , как мы скоро увидим

Примечание. Не следует объявлять ParameterResolver в обеих областях видимости для одного и того же типа параметра , иначе платформа JUnit будет жаловаться на эту двусмысленность .

В этой статье мы увидим, как написать и использовать два расширения для внедрения объектов Person : одно, которое вводит «хорошие» данные (называемое ValidPersonParameterResolver ), и другое, которое вводит «плохие» данные ( InvalidPersonParameterResolver ). Мы будем использовать эти данные для модульного тестирования класса PersonValidator , который проверяет состояние объекта Person .

3. Напишите расширения

Теперь, когда мы понимаем, что такое расширение ParameterResolver , мы готовы написать:

  • тот, который предоставляет допустимые объекты Person ( ValidPersonParameterResolver ), и
  • тот, который предоставляет недопустимые объекты Person ( InvalidPersonParameterResolver )

3.1. Валидперсонпараметрресолвер

public class ValidPersonParameterResolver implements ParameterResolver {

public static Person[] VALID_PERSONS = {
new Person().setId(1L).setLastName("Adams").setFirstName("Jill"),
new Person().setId(2L).setLastName("Baker").setFirstName("James"),
new Person().setId(3L).setLastName("Carter").setFirstName("Samanta"),
new Person().setId(4L).setLastName("Daniels").setFirstName("Joseph"),
new Person().setId(5L).setLastName("English").setFirstName("Jane"),
new Person().setId(6L).setLastName("Fontana").setFirstName("Enrique"),
};

Обратите внимание на массив VALID_PERSONS объектов Person . Это репозиторий действительных объектов Person , из которых один будет выбираться случайным образом при каждом вызове метода resolveParameter() платформой JUnit.

Наличие здесь допустимых объектов Person позволяет достичь двух целей:

  1. Разделение проблем между модульным тестом и данными, которые его управляют
  2. Повторное использование, если другие модульные тесты требуют действительных объектов Person для управления ими.
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}

Если тип параметра — Person , то расширение сообщает платформе JUnit, что поддерживает этот тип параметра, в противном случае оно возвращает false, говоря, что это не так.

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

@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = VALID_PERSONS[new Random().nextInt(VALID_PERSONS.length)];
}
return ret;
}

Случайный объект Person возвращается из массива VALID_PERSONS . Обратите внимание, что resolveParameter() вызывается платформой JUnit только в том случае, если supportsParameter() возвращает true .

3.2. Инвалидперсонпараметрресолвер

public class InvalidPersonParameterResolver implements ParameterResolver {
public static Person[] INVALID_PERSONS = {
new Person().setId(1L).setLastName("Ad_ams").setFirstName("Jill,"),
new Person().setId(2L).setLastName(",Baker").setFirstName(""),
new Person().setId(3L).setLastName(null).setFirstName(null),
new Person().setId(4L).setLastName("Daniel&").setFirstName("{Joseph}"),
new Person().setId(5L).setLastName("").setFirstName("English, Jane"),
new Person()/*.setId(6L).setLastName("Fontana").setFirstName("Enrique")*/,
};

Обратите внимание на массив объектов Person INVALID_PERSONS . Как и в случае с ValidPersonParameterResolver , этот класс содержит хранилище «плохих» (т. е. недопустимых) данных для использования модульными тестами, чтобы гарантировать, например, что PersonValidator.ValidationExceptions правильно выдаются при наличии недопустимых данных: ``

@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
Object ret = null;
if (parameterContext.getParameter().getType() == Person.class) {
ret = INVALID_PERSONS[new Random().nextInt(INVALID_PERSONS.length)];
}
return ret;
}

@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
boolean ret = false;
if (parameterContext.getParameter().getType() == Person.class) {
ret = true;
}
return ret;
}

Остальная часть этого класса, естественно, ведет себя точно так же, как его «хороший» аналог.

4. Объявите и используйте расширения

Теперь, когда у нас есть два ParameterResolver , пришло время их использовать. Давайте создадим тестовый класс JUnit для PersonValidator с именем PersonValidatorTest .

Мы будем использовать несколько функций, доступных только в JUnit Jupiter:

  • @ DisplayName — это имя, которое отображается в отчетах об испытаниях, и гораздо более понятное для человека.
  • @ Nested — создает вложенный тестовый класс с собственным жизненным циклом теста, отдельным от родительского класса.
  • @ RepeatedTest — тест повторяется количество раз, указанное атрибутом value (10 в каждом примере)

Используя классы @ Nested , мы можем тестировать как действительные, так и недопустимые данные в одном и том же тестовом классе, в то же время сохраняя их полностью изолированными друг от друга:

@DisplayName("Testing PersonValidator")
public class PersonValidatorTest {

@Nested
@DisplayName("When using Valid data")
@ExtendWith(ValidPersonParameterResolver.class)
public class ValidData {

@RepeatedTest(value = 10)
@DisplayName("All first names are valid")
public void validateFirstName(Person person) {
try {
assertTrue(PersonValidator.validateFirstName(person));
} catch (PersonValidator.ValidationException e) {
fail("Exception not expected: " + e.getLocalizedMessage());
}
}
}

@Nested
@DisplayName("When using Invalid data")
@ExtendWith(InvalidPersonParameterResolver.class)
public class InvalidData {

@RepeatedTest(value = 10)
@DisplayName("All first names are invalid")
public void validateFirstName(Person person) {
assertThrows(
PersonValidator.ValidationException.class,
() -> PersonValidator.validateFirstName(person));
}
}
}

Обратите внимание, как мы можем использовать расширения ValidPersonParameterResolver и InvalidPersonParameterResolver в одном и том же основном тестовом классе, объявляя их только на уровне класса @ Nested . Попробуйте это с JUnit 4! (Осторожно, спойлер: вы не можете этого сделать!)

5. Вывод

В этой статье мы рассмотрели, как написать два расширения ParameterResolver — для обслуживания действительных и недействительных объектов. Затем мы рассмотрели, как использовать эти две реализации ParameterResolver в модульном тесте.

Как всегда, код доступен на Github .

А если вы хотите узнать больше о модели расширения JUnit Jupiter, ознакомьтесь с Руководством пользователя JUnit 5 или частью 2 моего руководства на developerWorks .