本文部分代码来自 JUint 官方文档
JUnit5 可以通过 ParameterizedTest
和 SourceArguments 两个 annotation 来参数化一个单元测试。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class Junit5Test {
@ParameterizedTest
@ValueSource(ints = {1, 0, -1, Integer.MIN_VALUE, Integer.MAX_VALUE})
void parameterizedTest(int num) {
assertEquals("" + num, String.valueOf(num));
}
}
而上方代码中的 @ValueSource
就是 Source Argument,用于标注单元测试的参数。每个参数通过测试方法的参数传入。
@ValueSource 除了支持 ints 外,还支持:
short
byte
int
long
float
double
char
boolean
java.lang.String
java.lang.Class
除了 @ValueSource
之外,JUnit5 还提供了其他的 SourceArguments。例如:
@NullSource
@EmptySource
@MethodSource
@CsvSource
@ArgumentsSource
详细用法可以参考后文或者官方文档:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources
参数化的意识是,使得一个单元测试可以根据外部参数执行不同的测试。在编码单元测试时,一个频繁遇到的场景是,一段代码你需要测试各种不同的场景。
例如,如果你的代测代码接受数字作为输入,那么通常你需要测试,0、正数、负数、最大值、最小值。
或者,如果你的代测代码接受 List 作为输入,那么你就需要测试 null, 空 List、只有一个 Item 和有多个 Item 的情况。
如果单元测试无法参数化,你可能不得不复制粘贴好几次,把同一段单元测试代码复制几次,再修改相应的输入数据。又或者,你可以选择写个循环,遍历你的测试数据。
然而这样做都不够方便。第一种方法重复劳动太多,不符合 DRY 的代码原则。第二种方法,若中间的某个测试数据测出了问题,不能直观地看到是哪一组数据测出了问题。
而 JUnit5 的单元测试参数化,就是解决这样的困扰。在上方的 TL;DR 中,我通过 @ParameterizedTest 和 @ValueSource 定义了一个单元测试的输入数据。如果你在 IDE 里运行,你可以看到没一组数据都单独列出来,非常清晰明了。
你可以通过 @ValueSource 定义一组简单的测试数据。 @ValueSource 支持 8 中基本类型、String 字符串和 Class 类型。
short
byte
int
long
float
double
char
boolean
java.lang.String
java.lang.Class
下方代码演示了如何定义一组 Class 类型的测试参数。
@ParameterizedTest
@ValueSource(classes = {String.class, TimeUnit.class, RuntimeException.class})
void parameterizedTestWithClasses(Class clazz) {
System.out.println(clazz.getCanonicalName());
}
JUnit5 提供了关于 null 值和空值的数据源——@NullSource、@EmptySource 和 @NullAndEmptySource。
其中空值是指:
空字符串: ""
空数组: String[]、int[] 甚至二维数组 char[][]
空容器: 空的 List, Set 或者 Map
@NullAndEmptySource 为 @NullSource 和 @EmtpySource 的组合 Annotation。
当然,通过 Annotation 去定义一个空的测试参数似乎有点鸡肋。这三个数据源的主要用途是与 @ValueSource 一起使用。由于 Java 语法的本身限制,@ValueSource 是无法接受 null 值的,而 null 和 empty 又是单元测试中必不可少的 boundry case。为了解决这个问题,@NullSource、@EmptySource 和 @NullAndEmptySource 应运而生。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"A", "B", "C"})
void parameterizedTestForString(String str) {
System.out.println(str);
}
枚举,也是单元测试的常客。你的 if 和 switch 肯定经常与 Enum 配合使用,对吧?
@ParameterizedTest
@EnumSource
void parameterizedTestForEnum(TimeUnit unit) {
System.out.println(unit.name());
}
JUnit5 提供了 @EnumSource。@EnumSource 会根据方法的输入参数判断你要测试的枚举类型。然后注入所有的枚举值。上方的例子中,JUnit5 会生成 TimeUnit 的 NANOSECONDS、MICROSECONDS、MILLISECONDS等一共7个测试用例。
如果你不想测试所有枚举值,@EnumSource 接受 names 参数,用于指定需要测试的枚举值。
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void parameterizedTestForEnum(TimeUnit unit) {
System.out.println(unit.name());
}
而有时候,枚举的值太多,相比指定你想测试什么,你可能更想指定你不想测试什么。@EnumSource 提供了 mode 参数。该参数指定你想如何匹配枚举值。mode 支持 INCLUDE、EXCLUDE、MATCH_ALL 或 MATCH_ANY。嗯,顾名思义,不多做解释。
上述的众多 Argument Source,有一个比较尴尬的缺陷。它们只能指定一维的测试数据。如果你想指定多维的数据,或者你想指定输入数据和测试结果,事情就比较尴尬了。为了解决这个问题,你可以使用 @CsvSource 。@CsvSource 接受多行 csv 格式的数据,并根据 csv 生成单元测试。
@ParameterizedTest
@CsvSource({
"Apple , 5 ",
"Banana , 6 ",
"Watermelon, 10"
})
void parameterizedTestForCsv(String word, int len) {
assertEquals(len, word.length());
}
代码相当直观,不用多做解释。但有几点需要注意
@CsvSource({ "apple, 'lemon, lime'" })
@CsvSource({ "apple, ''" })
@CsvSource({ "apple, " })
或者指定 nullValues 参数:
@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL")
你还可以指定 CSV 文件:
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
参数 numLinesToSkip 指定跳过文件内容的多少行。一般地, CSV 文件的第一行为列名,而不是实际数据。
为了应对更复杂的需求,JUnit5 提供了更灵活的 Argument Source 接口, @ArgumentsSource。我们可以通过在 @ArgumentsSource 中指定一个 ArgumentsProvider 的实现类,该类通过实现 provideArguments 方法提供任意你需要的测试参数。
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
JUnit5 提供了各种形式的参数类型转换能力。自动类型转换能让你更随心所意的编写测试,而不用被 Java 的强类型特性所扰。
数据类型可以自动向上拓宽。例如 int 可以被自动转换成 long、float 或者 double。具体的转换规则参见:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2
JUnit5 还提供了各种方便的隐式类型转换。例如:
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
assertNotNull(argument.name());
}
在上述类型中,字符串 "SECONDS" 会被自动转换成 ChronoUnit。下方是 JUnit5 官方提供的内建类型转换表。
Target Type | Example |
---|---|
boolean /Boolean |
"true" → true |
byte /Byte |
"15" , "0xF" , or "017" → (byte) 15 |
char /Character |
"o" → 'o' |
short /Short |
"15" , "0xF" , or "017" → (short) 15 |
int /Integer |
"15" , "0xF" , or "017" → 15 |
long /Long |
"15" , "0xF" , or "017" → 15L |
float /Float |
"1.0" → 1.0f |
double /Double |
"1.0" → 1.0d |
Enum subclass |
"SECONDS" → TimeUnit.SECONDS |
java.io.File |
"/path/to/file" → new File("/path/to/file") |
java.lang.Class |
"java.lang.Integer" → java.lang.Integer.class (use $ for nested classes, e.g. "java.lang.Thread$State" ) |
java.lang.Class |
"byte" → byte.class (primitive types are supported) |
java.lang.Class |
"char[]" → char[].class (array types are supported) |
java.math.BigDecimal |
"123.456e789" → new BigDecimal("123.456e789") |
java.math.BigInteger |
"1234567890123456789" → new BigInteger("1234567890123456789") |
java.net.URI |
"https://junit.org/" → URI.create("https://junit.org/") |
java.net.URL |
"https://junit.org/" → new URL("https://junit.org/") |
java.nio.charset.Charset |
"UTF-8" → Charset.forName("UTF-8") |
java.nio.file.Path |
"/path/to/file" → Paths.get("/path/to/file") |
java.time.Duration |
"PT3S" → Duration.ofSeconds(3) |
java.time.Instant |
"1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0) |
java.time.LocalDateTime |
"2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000) |
java.time.LocalDate |
"2017-03-14" → LocalDate.of(2017, 3, 14) |
java.time.LocalTime |
"12:34:56.789" → LocalTime.of(12, 34, 56, 789_000_000) |
java.time.MonthDay |
"--03-14" → MonthDay.of(3, 14) |
java.time.OffsetDateTime |
"2017-03-14T12:34:56.789Z" → OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.OffsetTime |
"12:34:56.789Z" → OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.Period |
"P2M6D" → Period.of(0, 2, 6) |
java.time.YearMonth |
"2017-03" → YearMonth.of(2017, 3) |
java.time.Year |
"2017" → Year.of(2017) |
java.time.ZonedDateTime |
"2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.ZoneId |
"Europe/Berlin" → ZoneId.of("Europe/Berlin") |
java.time.ZoneOffset |
"+02:30" → ZoneOffset.ofHoursMinutes(2, 30) |
java.util.Currency |
"JPY" → Currency.getInstance("JPY") |
java.util.Locale |
"en" → new Locale("en") |
java.util.UUID |
"d043e930-7b3b-48e3-bdbe-5a3ccfb833db" → UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db") |
你还可以定义更多的类型转换。JUnit5 支持隐式和显式两种风格的类型转换定义。
这种方式的类型转换和上方 JUnit 自己提供的转换类似,都是把 String 自动转换成其他类型对象。
当 JUnit5 发现你提供了 String 类型的参数,却需要其他类型的实际数据时。如果该类定义了:
则 Junit5 会自动帮你把字符串转换成对应类型的实例。
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
public class Book {
private final String title;
private Book(String title) {
this.title = title;
}
public static Book fromTitle(String title) {
return new Book(title);
}
public String getTitle() {
return this.title;
}
}
上方的代码演示了 “42 Cats” 通过静态的 fromTitle() 转换 Book 实例。
嗯...不多说了,直接上代码吧。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
if (source instanceof Enum<?>) {
return ((Enum<?>) source).name();
}
return String.valueOf(source);
}
}
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return source.length();
}
}
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests