如何在JUnit 5中实现JUnit 4参数化测试? [英] How to implement JUnit 4 parameterized tests in JUnit 5?

查看:267
本文介绍了如何在JUnit 5中实现JUnit 4参数化测试?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

解决方案

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

TL; DR

要编写一个参数化测试,该测试根据大小写指定一个值,以测试您的问题, org.junit.jupiter.params.provider.MethodSource 应该做这份工作.

@MethodSource允许您引用一种或多种测试方法 班级.每个方法必须返回StreamIterableIterator或数组 论据.另外,每个方法都不能接受任何参数. 默认情况下,此类方法必须是静态的,除非测试类为 用@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

这是我实际上与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 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

如果您在测试方法中具有单个方法参数,并且您可以从这些内置类型之一(字符串,整型,长整型,双精度型)中的表示参数的来源@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具有一些优点:更清晰,更短.
实际上,参数是在测试方法的上方定义的,不需要创建夹具方法,该方法还可能会生成未使用"的警告.
但是它在映射类型方面也有一个重要的限制.
您必须提供一个String数组.该框架提供了转换功能,但受到限制.

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

并非如此,您必须在以下两种情况之间做出选择: 通过为框架未使用@MethodSource和工厂方法执行的转换创建自定义转换器(ArgumentConverter子类),从而保持@CsvSource的灵活性. 返回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指定要用于特定参数的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);
    }

}

In JUnit 4 it was easy to test invariants across a bunch of classes by using the @Parameterized annotation. The key thing is that a collection of tests are being run against a single list of arguments.

How to replicate this in JUnit 5, without using JUnit-vintage?

@ParameterizedTest is not applicable to a test class. @TestTemplate sounded like it might be appropriate, but that annotation's target is also a method.


An example of such a JUnit 4 test is:

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

解决方案

The parameterized test feature in JUnit 5 doesn't provide the exact same features than those provided by JUnit 4.
New features with more flexibility were introduced... but it also lost the JUnit4 feature where the parameterized test class uses the parameterized fixtures/assertions at the class level that is for all test methods of the class.
Defining @ParameterizedTest for each test method by specifying the "input" is so needed.
Beyond that lack I will present the main differences between the 2 versions and how to use parameterized tests in JUnit 5.

TL;DR

To write a parameterized test that specifies a value by case to test as your in your question, org.junit.jupiter.params.provider.MethodSource should do the job.

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

As JUnit 4, @MethodSource relies on a factory method and may also be used for test methods that specify multiple arguments.

In JUnit 5, it is the way of writing parameterized tests the closest to JUnit 4.

JUnit 4 :

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

JUnit 5 :

private static Stream<Arguments> data() {

Main improvements :

  • Collection<Object[]> is become Stream<Arguments> that provides more flexibility.

  • the way of binding the factory method to the test method differs a little.
    It is now shorter and less error prone : no more requirement to create a constructor and declares field to set the value of each parameter. The binding of the source is done directly on the parameters of the test method.

  • With JUnit 4, inside a same class, one and only one factory method has to be declared with @Parameters.
    With JUnit 5, this limitation is lifted : multiple methods may indeed be used as factory method.
    So, inside the class, we can so declare some test methods annotated with @MethodSource("..") that refer different factory methods.

For example here is a sample test class that asserts some addition computations :

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

To upgrade existing parameterized tests from JUnit 4 to JUnit 5, @MethodSource is a candidate to consider.


Summarize

@MethodSource has some strengths but also some weaknesses.
New ways to specify sources of the parameterized tests were introduced in JUnit 5.
Here some additional information (far being exhaustive) about them that I hope could give a broad idea on how deal with in a general way.

Introduction

JUnit 5 introduces parameterized tests feature in these terms :

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.

Dependency requirement

Parameterized tests feature is not included in the junit-jupiter-engine core dependency.
You should add a specific dependency to use it : junit-jupiter-params.

If you use Maven, this is the dependency to declare :

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

Sources available to create data

Contrary to JUnit 4, JUnit 5 provides multiple flavors and artifacts to write parameterized tests
The ways to favor depend generally on the source of data you want to use.

Here are the source types proposed by the framework and described in the documentation :

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

Here are the 3 main sources I actually use with JUnit 5 and I will present:

  • @MethodSource
  • @ValueSource
  • @CsvSource

I consider them as basic as I write parameterized tests. They should allow to write in JUnit 5, the type of JUnit 4 tests that you described.
@EnumSource, @ArgumentsSource and @CsvFileSource may of course be helpful but they are more specialized.

Presentation of @MethodSource, @ValueSource and @CsvSource

1) @MethodSource

This type of source requires to define a factory method.
But it also provides much flexibility.

In JUnit 5, it is the way of writing parameterized tests the closest to JUnit 4.

If you have a single method parameter in the test method and you want to use any type as source, @MethodSource is a very good candidate.
To achieve it, define a method that returns a Stream of the value for each case and annotate the test method with @MethodSource("methodName") where methodName is the name of this data source method.

For example, you could write :

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

}

If you have multiple method parameters in the test method and you want to use any type as source, @MethodSource is also a very good candidate.
To achieve it, define a method that returns a Stream of org.junit.jupiter.params.provider.Arguments for each case to test.

For example, you could write :

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

If you have a single method parameter in the test method and you may represent the source of the parameter from one of these built-in types (String, int, long, double), @ValueSource suits.

@ValueSource defines indeed these attributes :

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

You could for example use it in this way :

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

}

Beware 1) you must not specify more than one annotation attribute.
Beware 2) The mapping between the source and the parameter of the method can be done between two distinct types.
The type String used as source of data allows particularly, thanks to its parsing, to be converted into multiple other types.

3) @CsvSource

If you have multiple method parameters in the test method, a @CsvSource may suit.
To use it, annotate the test with @CsvSource and specify in a array of String each case.
Values of each case are separated by a comma.

Like @ValueSource, the mapping between the source and the parameter of the method can be done between two distinct types.
Here is an example that illustrates that :

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

These source types serve a very classic requirement : mapping from the source to multiple method parameters in the test method.
But their approach is different.

@CsvSource has some advantages : it is clearer and shorter.
Indeed, parameters are defined just above the tested method, no requirement to create a fixture method that may in addition generate "unused" warnings.
But it also has an important limitation concerning mapping types.
You have to provide an array of String. The framework provides conversion features but it is limited.

To summarize, while the String provided as source and the parameters of the test method have the same type (String->String) or rely on built-in conversion (String->int for example), @CsvSource appears as the way to use.

As it is not the case, you have to make a choice between keeping the flexibility of @CsvSource by creating a custom converter (ArgumentConverter subclass) for conversions not performed by the framework or using @MethodSource with a factory method that returns Stream<Arguments>.
It has the drawbacks described above but it also has the great benefit to map out-of-the box any type from the source to the parameters.

Argument Conversion

About the mapping between the source (@CsvSource or @ValueSource for example) and the parameters of the test method, as seen, the framework allows to do some conversions if the types are not the same.

Here is a presentation of the two types of conversions :

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

For example in the previous example, an implicit conversion is done between String from source and int defined as parameter:

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

And here, an implicit conversion is done from String source to LocalDate parameter:

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

If for two types, no conversion is provided by the framework, which is the case for custom types, you should use an 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 provides a reference implementation for clients who need to create a specific 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.

Test method using this converter :

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

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

}

这篇关于如何在JUnit 5中实现JUnit 4参数化测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆