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

39
在JUnit 4中,通过使用@Parameterized注解,可以轻松地在一堆类之间测试不变量。关键是一组测试正在针对单个参数列表运行。

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

@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(){
       ...
   } 
}

{btsdaf} - Roland Weisleder
1个回答

47

JUnit 5的参数化测试功能不提供与JUnit 4完全相同的功能。它引入了更灵活的新功能......但也失去了JUnit 4的参数化测试类在类级别使用参数化固定值/断言,即适用于类的所有测试方法的特性。
需要为每个测试方法定义@ParameterizedTest并指定“输入”值。
除此之外,我将介绍这两个版本之间的主要差异以及如何在JUnit 5中使用参数化测试。

TL;DR

要编写一个参数化测试以指定要测试的案例值,可以使用org.junit.jupiter.params.provider.MethodSource

@MethodSource允许您引用测试类的一个或多个方法。每个方法必须返回Stream、Iterable、Iterator或参数数组。此外,每个方法都不能接受任何参数。默认情况下,这些方法必须是静态的,除非测试类被注释为

如果您只需要一个参数,可以直接返回参数类型的实例,正如以下示例所示。

与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

这里是我实际在JUnit 5中使用并将要介绍的3个主要来源:

  • @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

如果你在测试方法中只有一个参数,并且该参数的数据类型是以下内置类型之一(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 VS @MethodSource

这两种数据源类型都满足一个经典的需求:将测试方法中的多个参数映射到源数据。但它们的方法不同。

@CsvSource 有一些优点:它更清晰、更简短。
实际上,参数只需在被测试的方法上方进行定义,无需创建一个fixture方法,从而导致"未使用"警告。
但是,它在映射类型方面也有一个重要的限制。
您必须提供一个String数组。该框架提供了转换功能,但是有限制。

总之,当提供的源和测试方法的参数具有相同类型(例如:String->String),或者依赖于内置转换(例如:String->int)时,@CsvSource 看起来是使用的方式。

如果不是这种情况,则需要在灵活性(通过创建自定义转换器ArgumentConverter子类,用于由框架未执行的转换)和使用工厂方法返回Stream<Arguments>@MethodSource之间进行选择。它具有上述缺点,但是它也有一个很大的好处:@MethodSource可以直接映射任何类型的数据到参数中。

参数转换

关于源(例如:@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
.....
例如在上一个示例中,从源代码到参数中会进行 Stringint 之间的隐式转换。
@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。而不是使用隐式参数转换,您可以使用@ConvertWith注解明确指定要使用的ArgumentConverter,如以下示例所示。JUnit为需要创建特定ArgumentConverter的客户端提供了一个参考实现。 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);
    }

}

关于@MethodSource的一个小细节 - 它也可以用于多个参数和许多用例,只需使用org.junit.jupiter.params.provider。例如,类似Stream.of(Arguments.of(5, "stringValue", myOwnType))这样的代码将适用于像methodSig(int arg1, String arg2, MyOwnType arg3)这样的@Test方法。 - mkobit
{btsdaf} - davidxxx
1
{btsdaf} - Sled
1
{btsdaf} - mkobit
{btsdaf} - Sled
JUnit 4参数化测试支持的一个优点是@Before/@After方法。因为测试参数在@Before/@After中传递给Test类构造函数,所以我们可以在调用测试方法之前使用传递的参数执行一些操作(测试夹具)。我甚至不知道如何在JUnit 5中实现这一点。我想在这种情况下,TestTemplate可能会有用。 - jtonic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接