Как реализовать параметризованные тесты JUnit 4 в JUnit 5?

В JUnit 4 было легко протестировать инварианты для нескольких классов с помощью @Parameterized аннотаций. Ключевым моментом является то, что набор тестов выполняется с одним списком аргументов.

Как повторить это в JUnit 5, не используя JUnit-vintage?

@ParameterizedTest не относится к тестовому классу. @TestTemplate звучит так, как будто это может быть уместно, но цель этой аннотации также является методом.


Пример такого теста JUnit 4:

@RunWith( Parameterized.class )
public class FooInvariantsTest{

   @Parameterized.Parameters
   public static Collection<Object[]> data(){
       return new Arrays.asList(
               new Object[]{ new CsvFoo() ),
               new Object[]{ new SqlFoo() ),
               new Object[]{ new XmlFoo() ),
           );
   }

   private Foo fooUnderTest;


   public FooInvariantsTest( Foo fooToTest ){
        fooUnderTest = fooToTest;
   }

   @Test
   public void testInvariant1(){
       ...
   }

   @Test
   public void testInvariant2(){
       ...
   } 
}

2 ответа

Решение

Функция параметризованного тестирования в JUnit 5 не обеспечивает те же функции, что и в JUnit 4.
Были введены новые функции с большей гибкостью... но также утрачена функция JUnit 4, в которой параметризованный тестовый класс использует параметризованные фикстуры / утверждения на уровне класса, то есть для всех методов тестирования класса.
определяющий @ParameterizedTest для каждого метода тестирования, указав "вход" так необходимо.
Помимо этого недостатка, я расскажу об основных различиях между двумя версиями и о том, как использовать параметризованные тесты в JUnit 5.

TL; DR

Чтобы написать параметризованный тест, в котором будет указано значение для каждого конкретного теста, org.junit.jupiter.params.provider.MethodSource должен сделать работу.

@MethodSource позволяет ссылаться на один или несколько методов тестового класса. Каждый метод должен возвращать Stream, Iterable, Iteratorили массив аргументов. Кроме того, каждый метод не должен принимать никаких аргументов. По умолчанию такие методы должны быть статическими, если тестовый класс не помечен @TestInstance(Lifecycle.PER_CLASS),

Если вам нужен только один параметр, вы можете вернуть экземпляры типа параметра напрямую, как показано в следующем примере.

Как JUnit 4, @MethodSource опирается на фабричный метод и может также использоваться для методов тестирования, которые указывают несколько аргументов.

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

JUnit 4:

@Parameters
public static Collection<Object[]> data() {

JUnit 5:

private static Stream<Arguments> data() {

Основные улучшения:

  • Collection<Object[]> это стало Stream<Arguments> это обеспечивает большую гибкость.

  • способ привязки фабричного метода к тестовому методу немного отличается.
    Теперь он короче и менее подвержен ошибкам: больше нет необходимости создавать конструктор и объявляется поле для установки значения каждого параметра. Привязка к источнику осуществляется непосредственно по параметрам метода испытаний.

  • В JUnit 4 внутри одного класса один и только один фабричный метод должен быть объявлен с помощью @Parameters,
    В JUnit 5 это ограничение снято: в качестве заводского метода можно использовать несколько методов.
    Итак, внутри класса мы можем объявить некоторые тестовые методы с пометкой @MethodSource("..") которые ссылаются на разные фабричные методы.

Например, вот пример тестового класса, который утверждает некоторые дополнительные вычисления:

import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;    
import org.junit.jupiter.api.Assertions;

public class ParameterizedMethodSourceWithArgumentsTest {

  @ParameterizedTest
  @MethodSource("addFixture")
  void add(int a, int b, int result) {
     Assertions.assertEquals(result, a + b);
  }

  private static Stream<Arguments> addFixture() {
    return Stream.of(
      Arguments.of(1, 2, 3),
      Arguments.of(4, -4, 0),
      Arguments.of(-3, -3, -6));
  }
}

Чтобы обновить существующие параметризованные тесты с JUnit 4 до JUnit 5, @MethodSource является кандидатом для рассмотрения.


Суммировать

@MethodSource имеет свои сильные и слабые стороны.
Новые способы указать источники параметризованных тестов были введены в JUnit 5.
Вот некоторая дополнительная информация (далеко не исчерпывающая) о них, которая, я надеюсь, могла бы дать широкое представление о том, как действовать в целом.

Вступление

JUnit 5 представляет функцию параметризованных тестов в этих терминах:

Параметризованные тесты позволяют запускать тест несколько раз с разными аргументами. Они объявлены как обычные @Test методы, но используют @ParameterizedTest аннотация вместо. Кроме того, вы должны объявить как минимум один источник, который будет предоставлять аргументы для каждого вызова.

Требование зависимости

Параметризованные тесты не включены в junit-jupiter-engine основная зависимость.
Вы должны добавить определенную зависимость, чтобы использовать его: junit-jupiter-params,

Если вы используете Maven, это зависимость для объявления:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.0.0</version>
    <scope>test</scope>
</dependency>

Источники, доступные для создания данных

В отличие от JUnit 4, JUnit 5 предоставляет несколько вариантов и артефактов для написания параметризованных тестов.
Способы оплаты зависят, как правило, от источника данных, которые вы хотите использовать.

Вот типы источников, предложенные платформой и описанные в документации:

  • @ValueSource
  • @EnumSource
  • @MethodSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentsSource

Вот 3 основных источника, которые я на самом деле использую с JUnit 5, и я представлю:

  • @MethodSource
  • @ValueSource
  • @CsvSource

Я считаю их базовыми, так как пишу параметризованные тесты. Они должны позволять писать в JUnit 5, тип тестов JUnit 4, который вы описали.
@EnumSource, @ArgumentsSource а также @CsvFileSource может, конечно, быть полезным, но они более специализированы.

Презентация @MethodSource , @ValueSource а также @CsvSource

1) @MethodSource

Этот тип источника требует определения фабричного метода.
Но это также обеспечивает большую гибкость.

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

Если у вас есть один параметр метода в тестовом методе, и вы хотите использовать любой тип в качестве источника, @MethodSource это очень хороший кандидат.
Чтобы добиться этого, определите метод, который возвращает поток значения для каждого случая, и пометьте метод теста с помощью @MethodSource("methodName") где methodName Имя этого метода источника данных.

Например, вы можете написать:

import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

public class ParameterizedMethodSourceTest {

    @ParameterizedTest
    @MethodSource("getValue_is_never_null_fixture")
    void getValue_is_never_null(Foo foo) {
       Assertions.assertNotNull(foo.getValue());
    }

    private static Stream<Foo> getValue_is_never_null_fixture() {
       return Stream.of(new CsvFoo(), new SqlFoo(), new XmlFoo());
    }

}

Если у вас есть несколько параметров метода в методе теста, и вы хотите использовать любой тип в качестве источника, @MethodSource тоже очень хороший кандидат.
Чтобы достичь этого, определите метод, который возвращает поток org.junit.jupiter.params.provider.Arguments для каждого случая, чтобы проверить.

Например, вы можете написать:

import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;    
import org.junit.jupiter.api.Assertions;

public class ParameterizedMethodSourceWithArgumentsTest {

    @ParameterizedTest
    @MethodSource("getFormatFixture")
    void getFormat(Foo foo, String extension) {
        Assertions.assertEquals(extension, foo.getExtension());
    }

    private static Stream<Arguments> getFormatFixture() {
    return Stream.of(
        Arguments.of(new SqlFoo(), ".sql"),
        Arguments.of(new CsvFoo(), ".csv"),
        Arguments.of(new XmlFoo(), ".xml"));
    }
}

2) @ValueSource

Если у вас есть один параметр метода в методе test, и вы можете представлять источник параметра из одного из этих встроенных типов (String, int, long, double), @ValueSource костюмы.

@ValueSource действительно определяет эти атрибуты:

String[] strings() default {};
int[] ints() default {};
long[] longs() default {};
double[] doubles() default {};

Например, вы можете использовать его следующим образом:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class ParameterizedValueSourceTest {

    @ParameterizedTest
    @ValueSource(ints = { 1, 2, 3 })
    void sillyTestWithValueSource(int argument) {
        Assertions.assertNotNull(argument);
    }

}

Остерегайтесь 1) вы не должны указывать более одного атрибута аннотации.
Остерегайтесь 2) Отображение между источником и параметром метода может быть сделано между двумя различными типами.
Тип String использование в качестве источника данных позволяет, в частности, благодаря его синтаксическому анализу преобразовывать в несколько других типов.

3) @CsvSource

Если у вас есть несколько параметров метода в методе теста, @CsvSource может подойти.
Чтобы использовать это, аннотируйте тест с @CsvSource и указать в массиве String каждый случай.
Значения каждого случая разделяются запятой.

подобно @ValueSource отображение между источником и параметром метода может быть сделано между двумя различными типами.
Вот пример, который иллюстрирует это:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class ParameterizedCsvSourceTest {

    @ParameterizedTest
    @CsvSource({ "12,3,4", "12,2,6" })
    public void divideTest(int n, int d, int q) {
       Assertions.assertEquals(q, n / d);
    }

}

@CsvSource В.С. @MethodSource

Эти типы источников служат очень классическому требованию: сопоставление источника с несколькими параметрами метода в тестовом методе.
Но у них другой подход.

@CsvSource имеет ряд преимуществ: оно четче и короче.
Действительно, параметры определены чуть выше тестируемого метода, нет необходимости создавать метод фиксации, который может дополнительно генерировать "неиспользуемые" предупреждения.
Но это также имеет важное ограничение относительно типов отображения.
Вы должны предоставить массив String, Каркас предоставляет функции преобразования, но он ограничен.

Подводя итог, в то время как String предоставляется как источник, и параметры метода теста имеют одинаковый тип (String -> String) или полагаться на встроенное преобразование (String -> int например), @CsvSource появляется как способ использования.

Поскольку это не так, вы должны сделать выбор между сохранением гибкости @CsvSource путем создания собственного конвертера (ArgumentConverter подкласс) для преобразований, не выполняемых платформой или использующих @MethodSource с фабричным методом, который возвращает Stream<Arguments>,
У него есть недостатки, описанные выше, но он также имеет большое преимущество для сопоставления готового любого типа от источника к параметрам.

Преобразование аргумента

О сопоставлении между источниками (@CsvSource или же @ValueSource например) и параметры метода испытаний, как видно, каркас позволяет делать некоторые преобразования, если типы не совпадают.

Вот презентация двух типов конверсий:

3.13.3. Преобразование аргумента

Неявное преобразование

Для поддержки вариантов использования, таких как @CsvSource JUnit Jupiter предоставляет несколько встроенных неявных преобразователей типов. Процесс преобразования зависит от объявленного типа каждого параметра метода.

.....

String экземпляры в настоящее время неявно преобразуются в следующие целевые типы.

Target Type          |  Example
boolean/Boolean      |  "true" → true
byte/Byte            |  "1" → (byte) 1
char/Character       |  "o" → 'o'
short/Short          |  "1" → (short) 1
int/Integer          |  "1" → 1
.....

Например, в предыдущем примере неявное преобразование выполняется между String из источника и int определяется как параметр:

@CsvSource({ "12,3,4", "12,2,6" })
public void divideTest(int n, int d, int q) {
   Assertions.assertEquals(q, n / d);
}

И здесь неявное преобразование выполняется из String источник к LocalDate параметр:

@ParameterizedTest
@ValueSource(strings = { "2018-01-01", "2018-02-01", "2018-03-01" })
void testWithValueSource(LocalDate date) {
    Assertions.assertTrue(date.getYear() == 2018);
}

Если для двух типов среда не обеспечивает преобразование, как в случае пользовательских типов, следует использовать ArgumentConverter,

Явное преобразование

Вместо использования неявного преобразования аргументов вы можете явно указать ArgumentConverter использовать для определенного параметра, используя @ConvertWith аннотация, как в следующем примере.

JUnit предоставляет эталонную реализацию для клиентов, которым необходимо создать конкретную ArgumentConverter,

Конвертеры явных аргументов предназначены для реализации авторами тестов. Таким образом, junit-jupiter-params предоставляет только один явный конвертер аргументов, который также может служить эталонной реализацией: JavaTimeArgumentConverter, Используется через составную аннотацию JavaTimeConversionPattern,

Метод испытания с использованием этого конвертера:

@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
    assertEquals(2017, argument.getYear());
}

JavaTimeArgumentConverter класс преобразователя:

package org.junit.jupiter.params.converter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalQuery;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.jupiter.params.support.AnnotationConsumer;

/**
 * @since 5.0
 */
class JavaTimeArgumentConverter extends SimpleArgumentConverter
        implements AnnotationConsumer<JavaTimeConversionPattern> {

    private static final Map<Class<?>, TemporalQuery<?>> TEMPORAL_QUERIES;
    static {
        Map<Class<?>, TemporalQuery<?>> queries = new LinkedHashMap<>();
        queries.put(ChronoLocalDate.class, ChronoLocalDate::from);
        queries.put(ChronoLocalDateTime.class, ChronoLocalDateTime::from);
        queries.put(ChronoZonedDateTime.class, ChronoZonedDateTime::from);
        queries.put(LocalDate.class, LocalDate::from);
        queries.put(LocalDateTime.class, LocalDateTime::from);
        queries.put(LocalTime.class, LocalTime::from);
        queries.put(OffsetDateTime.class, OffsetDateTime::from);
        queries.put(OffsetTime.class, OffsetTime::from);
        queries.put(Year.class, Year::from);
        queries.put(YearMonth.class, YearMonth::from);
        queries.put(ZonedDateTime.class, ZonedDateTime::from);
        TEMPORAL_QUERIES = Collections.unmodifiableMap(queries);
    }

    private String pattern;

    @Override
    public void accept(JavaTimeConversionPattern annotation) {
        pattern = annotation.value();
    }

    @Override
    public Object convert(Object input, Class<?> targetClass) throws ArgumentConversionException {
        if (!TEMPORAL_QUERIES.containsKey(targetClass)) {
            throw new ArgumentConversionException("Cannot convert to " + targetClass.getName() + ": " + input);
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        TemporalQuery<?> temporalQuery = TEMPORAL_QUERIES.get(targetClass);
        return formatter.parse(input.toString(), temporalQuery);
    }

}

Мы всегда должны запускать тестовый пример с разными значениями для пограничного тестирования, один из способов - создать несколько тестовых примеров метода для другого набора входных значений, но он будет иметь много шаблонного кода и не является лучшей практикой или использованием Аннотация JUnit 5 @ParameterizedTest. Метод @Test будет вызываться несколько раз с разными значениями параметров каждый раз.

@ParameterizedTest предназначен только для обозначения того, что параметры этого тестового примера будут переданы во время выполнения, но чтобы объявить, откуда получать входные данные от @ParameterizedTest, требуется любой из следующих источников входных данных.

  • @ValueSource
  • @ Аргументы
  • @ArgumentsProvider
  • @ArgumentsSource
  • @CsvFileSource
  • @CsvSource
  • @EnumSource
  • @MethodSource

все примеры приведены здесь

Другие вопросы по тегам