JUnit 5中的参数化测试功能所提供的
功能与JUnit
4所提供的功能完全不同。引入了具有更大灵活性的新功能…但是它也失去了JUnit4功能,其中参数化测试类使用参数化的固定装置/断言在该类的所有测试方法的类级别上。因此需要通过指定“输入”来
定义
@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:
@Parameterspublic 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->
int),
@CsvSource但它是使用的方式。
并非如此,您必须在为
@CsvSource创建
ArgumentConverter非框架执行的转换创建自定义转换器(子类) 或
使用
@MethodSourcereturn的工厂方法来保持灵活性之间进行选择
Stream<Arguments>。
它具有上述缺点,但从源到参数的任何类型的开箱即用映射也具有很大的好处。
参数转换
关于源(
@CsvSource或
@ValueSource例如)和测试方法的参数之间的映射,如所看到的,如果类型不同,则框架允许进行一些转换。
以下是两种转换类型的介绍:
3.13.3。 参数转换
隐式转换
为了支持诸如的使用案例
@CsvSource,JUnit Jupiter提供了许多内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。.....
String实例当前隐式转换为以下目标类型。Target Type | Exampleboolean/Boolean | "true" → truebyte/Byte | "1" → (byte) 1char/Character | "o" → 'o'short/Short | "1" → (short) 1int/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;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); }}


