«1. Обзор

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

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

2. ParameterResolver

Введение параметров в методы тестирования можно было выполнить с помощью API JUnit 4, но его возможности были весьма ограничены. С помощью 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. FooTest

@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. ValidPersonParameterResolver

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"),
  };

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

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

  1. Separation of concerns between the unit test and the data that drives it
  2. Reuse, should other unit tests require valid Person objects to drive them
@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 должна проверять все зарегистрированные ParameterResolvers при разрешении параметров в пределах текущей области влияния.

@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. InvalidPersonParameterResolver

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")*/,
  };

«

@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;
}

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

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

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

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

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

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

@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));
        }
    }
}

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

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

5. Заключение

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

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