Внедрить параметры в юнит-тесты JUnit

Внедрить параметры в юнит-тесты JUnit

1. Overviewс

До JUnit 5, чтобы представить новую классную функцию, команде JUnit пришлось бы сделать это с основным API. С JUnit 5 команда решила, что пришло время выдвинуть возможность расширить ядро ​​API JUnit за пределы самого JUnit, основную философию JUnit 5 под названием «https://github.com/junit-team/junit5/wiki/Core-Principles». [предпочитаю точки расширения над функциями] ».

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

2. ParameterResolverс

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

Расширение имеетscope of influence, который активирует расширение, в зависимости от того,where оно объявлено.

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

  • уровень метода, где он активен только для этого метода, или

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

Note: you should not declare a ParameterResolverat both scopes for the same parameter type, or the JUnit Platform will complain about this ambiguity.

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

3. Write the Extensionsс

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

  • тот, который предоставляет объектыvalidPerson (ValidPersonParameterResolver), и

  • тот, который предоставляетinvalidPerson объектов (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"),
  };

Обратите внимание на массив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 должна проверять все зарегистрированныеParameterResolvers при разрешении параметровwithin the current scope of influence.

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

Обратите внимание на массивINVALID_PERSONS объектовPerson. Как и в случае с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. Объявите и используйте расширения

Теперь, когда у нас есть дваParameterResolvers, пора применить их. Давайте создадим тестовый класс 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 в модульном тесте.

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

И, если вы хотите узнать больше о модели расширения JUnit Jupiter, посмотритеJUnit 5 User’s Guide илиpart 2 of my tutorial on developerWorks.