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 позволяет достичь двух целей:
- Разделение проблем между модульным тестом и данными, которые его управляют
- Повторное использование, если другие модульные тесты требуют действительных объектов
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 .