如何在 JUnit 5 中实现 JUnit 4 参数化测试?

How to implement JUnit 4 parameterized tests in JUnit 5?

在 JUnit 4 中,通过使用 @Parameterized 注释可以很容易地在一堆 类 中测试不变量。关键是一组测试正在 运行 针对单个参数列表。

如何在不使用 JUnit-vintage 的情况下在 JUnit 5 中复制它?

@ParameterizedTest is not applicable to a test class. @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(){
       ...
   } 
}

JUnit 5 中的参数化测试功能提供的功能与 JUnit 4 提供的功能不完全相同。
引入了具有更大灵活性的新功能......但它也失去了 JUnit4 功能,其中参数化测试 class 在 class 级别使用参数化 fixtures/assertions 用于所有测试方法class.
因此需要通过指定 "input" 为每个测试方法定义 @ParameterizedTest
除此之外,我将介绍这两个版本之间的主要区别以及如何在 JUnit 5 中使用参数化测试。

TL;DR

要编写一个参数化测试,按案例指定一个值来测试你的问题, org.junit.jupiter.params.provider.MethodSource 应该可以完成这项工作。

@MethodSource allows you to refer to one or more methods of the test class. Each method must return a Stream, Iterable, Iterator, or array of arguments. In addition, each method must not accept any arguments. By default such methods must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).

If you only need a single parameter, you can return instances of the parameter type directly as demonstrated by the following example.

作为 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,在同一个 class 中,必须使用 @Parameters.
    声明一个并且只有一个工厂方法 在 JUnit 5 中,这个限制被取消了:多个方法确实可以用作工厂方法。
    因此,在 class 中,我们可以声明一些用 @MethodSource("..") 注释的测试方法,这些方法引用不同的工厂方法。

例如,这里有一个示例测试 class,断言了一些加法计算:

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 在这些术语中引入了 parameterized tests feature

Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular @Test methods but use the @ParameterizedTest annotation instead. In addition, you must declare at least one source that will provide the arguments for each invocation.

依赖性要求

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 提供了多种风格和工件来编写参数化测试
赞成的方式一般取决于你要使用的数据来源。

以下是框架提出并在 documentation 中描述的源类型:

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

以下是我在 JUnit 5 中实际使用的 3 个主要来源,我将介绍:

  • @MethodSource
  • @ValueSource
  • @CsvSource

我认为它们是我编写参数化测试的基础。他们应该允许在 JUnit 5 中编写,即您描述的 JUnit 4 测试类型。
@EnumSource@ArgumentsSource@CsvFileSource 当然可能会有帮助,但它们更专业。

@MethodSource@ValueSource@CsvSource

的演示文稿

1) @MethodSource

此类源需要定义工厂方法。
但它也提供了很大的灵活性。

在JUnit 5中,它是最接近JUnit 4的编写参数化测试的方式。

如果你在测试方法中有一个单一方法参数并且你想使用任何类型作为源,@MethodSource 是一个很好的候选人。
要实现它,定义一个方法,该方法 returns 每个案例的值流,并用 @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也是一个很好的人选。
要实现它,请定义一个方法,该方法 returns 一个 org.junit.jupiter.params.provider.Arguments 的 Stream 用于每个要测试的案例。

例如,您可以这样写:

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

如果您在测试方法中有一个单一方法参数,您可以表示来自这些内置类型之一的参数来源(字符串, 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 VS @MethodSource

这些源类型满足非常 classic 的要求:从源映射到测试方法中的多个方法参数
但他们的做法不同。

@CsvSource 有一些优点:它更清晰、更短。
实际上,参数是在测试方法之上定义的,不需要创建可能另外生成 "unused" 警告的夹具方法。
但它在映射类型方面也有一个重要的限制。
您必须提供 String 的数组。该框架提供了转换功能,但它是有限的。

总而言之,作为源提供的String和测试方法的参数具有相同的类型(String->String)或依赖内置转换(例如String->int),@CsvSource作为使用方式出现。

事实并非如此,您必须在两者之间做出选择 通过为框架 使用 [=26= 执行的转换创建自定义转换器 (ArgumentConverter subclass) 来保持 @CsvSource 的灵​​活性] 使用工厂方法 returns Stream<Arguments>.
它具有上述缺点,但它也有很大的好处,可以将任何类型从源映射到参数。

参数转换

关于源(例如@CsvSource@ValueSource)和测试方法的参数之间的映射,如上所示,如果类型不是一样。

Here是两种类型转换的介绍:

3.13.3. Argument Conversion

Implicit Conversion

To support use cases like @CsvSource, JUnit Jupiter provides a number of built-in implicit type converters. The conversion process depends on the declared type of each method parameter.

.....

String instances are currently implicitly converted to the following target types.

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.

Explicit Conversion

Instead of using implicit argument conversion you may explicitly specify an ArgumentConverter to use for a certain parameter using the @ConvertWith annotation like in the following example.

JUnit 为需要创建特定 ArgumentConverter.

的客户提供参考实现

Explicit argument converters are meant to be implemented by test authors. Thus, junit-jupiter-params only provides a single explicit argument converter that may also serve as a reference implementation: JavaTimeArgumentConverter. It is used via the composed annotation JavaTimeConversionPattern.

使用此转换器的测试方法:

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

JavaTimeArgumentConverter 转换器 class :

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

}