Spring Boot 使用 JUnit5 提供的 @ParameterizedTest
注解实现参数化测试,同时要配合其它注解完成参数源配置。
一、自定义测试执行名称
@ParameterizedTest
默认的测试执行名称格式为 [序号]参数1=XXX, 参数2=YYY...
,可以通过修改 name
属性自定义测试执行名称。
@ParameterizedTest(name = "第 {index} 次测试,参数:{0}")
@ValueSource(ints = { 1, 10, 100 })
public void test(int value) {
Assertions.assertTrue(value < 100);
}
测试执行名称为:
第 1 次测试,参数:1
第 2 次测试,参数:10
第 3 次测试,参数:100
如果参数有多个,则依次为: {0}
、 {1}
、 {2}
…
二、参数数据源
1. @ValueSource
@ValueSource
数据源支持以下类型的数组:
short
byte
int
long
float
double
char
java.lang.String
java.lang.Class
以 int
类型为例,以下方法会测试 3 次,第 3 次参数为 100 时测试结果为 Fail。
@ParameterizedTest
@ValueSource(ints = { 1, 10, 100 })
public void test(int value) {
Assertions.assertTrue(value < 100);
}
2. @NullSource
在使用字符串作为入参时,有时可能会用到 null
,不能直接将 null
写入 @ValueSource
注解的 strings
数组中(编译器会报错)。
@ValueSource(strings = { null, "X", "Y", "Z" })
正确的方法是使用 @NullSource
注解。
@ParameterizedTest
@NullSource
@ValueSource(strings = { "X", "Y", "Z" })
public void test(String value) {
System.out.println("Param: " + value);
}
3. @EmptySource
同 @NullSource
类似,使用字符串作为入参时如果需要使用空字符串,可以使用 @EmptySource
。
@ParameterizedTest
@EmptySource
@ValueSource(strings = { "X", "Y", "Z" })
public void test(String value) {
System.out.println("Param: " + value);
}
与 @NullSource
不同的是,可以直接在 @ValueSource
注解的 strings
数组中写空字符串参数,编译器不会报错。
@ValueSource(strings = { "", "X", "Y", "Z" })
4. @NullAndEmptySource
如果要同时使用 null
和空字符串作为测试方法的入参,可以使用 @NullAndEmptySource
注解。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { "X", "Y", "Z" })
public void test(String value) {
System.out.println("Param: " + value);
}
5. @EnumSource
@EnumSource
是枚举数据源,可以让一个枚举类中的全部或部分值作为测试方法的入参。
- 先定义一个枚举类
public enum Type {
ALPHA, BETA, GAMMA, DELTA
}
- 在测试方法上添加
@EnumSource
注解,JUnit 根据测试方法参数类型判断使用哪个枚举。
@ParameterizedTest
@EnumSource
public void test(Type type) {
System.out.println("Param: " + type);
}
- 如果只想使用枚举中的部分值,可以在
@EnumSource
注解的names
属性中指定,如果names
属性包含不存在的枚举值则运行时会报错。
@ParameterizedTest
@EnumSource(names = { "BETA", "DELTA" })
public void test(Type type) {
System.out.println("Param: " + type);
}
- 也可以通过设置
@EnumSource
注解的mode
属性为EnumSource.Mode.EXCLUDE
指定不使用哪些枚举值。
@ParameterizedTest
@EnumSource(mode = Mode.EXCLUDE, names = { "BETA", "DELTA" })
public void test(Type type) {
System.out.println("Param: " + type);
}
6. @MethodSource
@MethodSource
注解可以指定一个方法名称,使用该方法返回的元素集合作为测试方法的入参,方法必须返回 Stream
类型。
@ParameterizedTest
@MethodSource("paramProvider")
public void test(String param) {
System.out.println("Param: " + param);
}
public static Stream<String> paramProvider() {
return Stream.of("X", "Y", "Z");
}
以上是个静态方法,且此方法与测试方法在同一个类中,如果不在同一个类中则需要指定静态方法的全路径: package.类名#方法名
。
@ParameterizedTest
@MethodSource("com.example.demo.DemoUnitTest#paramProvider")
public void test(String param) {
System.out.println("Param: " + param);
}
如果参数数据源不是静态方法而是实例方法,则需要使用 @TestInstance
注解。
7. @CsvSource
以上数据源都只针对一个参数的测试方法, @CsvSource
可以处理多个参数的测试方法。
@ParameterizedTest
@CsvSource({
"2021/12/01, Wednesday, Sunny",
"2021/12/10, Friday, Rainy",
"2021/12/13, Monday, Chilly"
})
public void test(String date, String dayOfWeek, String weather) {
System.out.println(date + " -- " + dayOfWeek + " -- " + weather);
}
@CsvSource
注解提供了一个 nullValues
属性,可以将指定字符串替代成 null
。
@ParameterizedTest
@CsvSource(value = {
"2021/12/01, Wednesday, Sunny",
"2021/12/10, Friday, ",
"2021/12/13, Monday, Chilly"
}, nullValues = "")
public void test(String date, String dayOfWeek, String weather) {
System.out.println(date + " -- " + dayOfWeek + " -- " + weather);
}
8. @CsvFileSource
测试数据量大时直接将测试数据写入源文件不太合适,可以使用 @CsvFileSource
代替 @CsvSource
,指定对应的 csv 文件作为数据源,可以使用 numLinesToSkip
属性指定跳过的行数(跳过表头)。
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/data.csv", numLinesToSkip = 1, delimiter = ',', nullValues = "")
public void test(String date, String dayOfWeek, String weather) {
System.out.println(date + " -- " + dayOfWeek + " -- " + weather);
}
CSV 文件如下:
日期,星期几,天气
2021/12/1,Wednesday,Sunny
2021/12/10,Friday,
2021/12/13,Monday,Chilly
9. @ArgumentsSource
如果以上数据源都不能满足测试需求,可以开发实现 ArgumentsProvider
接口的实现类作为自定义参数数据源。
package com.example.demo;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
public class CustomArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext arg0) throws Exception {
return Stream.of(Arguments.of(1), Arguments.of("2"), Arguments.of(true));
}
}
然后使用 @ArgumentsSource
指定数据源。
@ParameterizedTest
@ArgumentsSource(CustomArgumentsProvider.class)
public void test(Object param) {
System.out.println(param);
}
三、参数转换
如果参数数据源的数据类型和测试方法参数的数据类型不一致,可以指定类型转换器进行数据类型转换。
@ParameterizedTest
@ValueSource(strings = { "2021/12/01", "2021/12/10" })
public void test(@JavaTimeConversionPattern("yyyy/MM/dd") LocalDate date) {
System.out.println(date);
}
以上代码将数据源的字符串类型通过转换器转换成了日期类型。
1. 自定义参数转换器
自定义参数转换器需要实现 ArgumentConverter
接口。
package com.example.demo;
import java.util.regex.Pattern;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.converter.ArgumentConversionException;
import org.junit.jupiter.params.converter.ArgumentConverter;
public class CustomConversionPattern implements ArgumentConverter {
@Override
public Object convert(Object arg0, ParameterContext arg1) throws ArgumentConversionException {
boolean isInteger = Pattern.matches("^[1-9][0-9]*$", arg0.toString());
if (!isInteger) {
throw new IllegalArgumentException();
}
return Integer.valueOf(arg0.toString());
}
}
在测试方法参数前使用 @ConvertWith
注解制定自定义的参数转换器。
@ParameterizedTest
@ValueSource(strings = { "1", "10", "100" })
public void test(@ConvertWith(CustomConversionPattern.class) Integer value) {
System.out.println(value);
}
四、字段聚合
如果数据源中每条数据有多个字段,按照之前所示需要在测试方法中定义与字段数量相等的参数,非常不方便,可以通过 ArgumentsAccessor
获取数据源中所有字段,CSV 字段实际上存储在 ArgumentsAccessor
实例内部的一个 Object 数组中。
@ParameterizedTest
@CsvSource({
"2021/12/01, Wednesday, Sunny",
"2021/12/10, Friday, Rainy",
"2021/12/13, Monday, Chilly"
})
public void test(ArgumentsAccessor argumentsAccessor) {
LocalDate date = LocalDate.parse(argumentsAccessor.getString(0), DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String dayOfWeek = argumentsAccessor.getString(1);
String weather = argumentsAccessor.getString(2);
System.out.println(date + " -- " + dayOfWeek + " -- " + weather);
}
1. 自定义参数聚合器
将 CSV 中每条数据封装成一个类对象。
package com.example.demo;
import java.time.LocalDate;
public class TestData {
private LocalDate date;
private String dayOfWeek;
private String weather;
// Getter Setter 略
}
自定义聚合器。
package com.example.demo;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
public class TestDataAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor arg0, ParameterContext arg1)
throws ArgumentsAggregationException {
TestData testData = new TestData();
testData.setDate(LocalDate.parse(arg0.getString(0), DateTimeFormatter.ofPattern("yyyy/MM/dd")));
testData.setDayOfWeek(arg0.getString(1));
testData.setWeather(arg0.getString(2));
return testData;
}
}
使用 @AggregateWith
注解指定聚合器。
package com.example.demo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.provider.CsvSource;
public class DemoUnitTest {
@ParameterizedTest
@CsvSource({
"2021/12/01, Wednesday, Sunny",
"2021/12/10, Friday, Rainy",
"2021/12/13, Monday, Chilly"
})
public void test(@AggregateWith(TestDataAggregator.class) TestData testData) {
System.out.println(testData.getDate() + " -- " + testData.getDayOfWeek() + " -- " + testData.getWeather());
}
}
2. 自定义聚合器注解
将上一步中 @AggregateWith(TestDataAggregator.class)
封装成自定义注解。
package com.example.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.params.aggregator.AggregateWith;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(TestDataAggregator.class)
public @interface CsvToTestData {
}
使用自定义聚合器注解。
@ParameterizedTest
@CsvSource({
"2021/12/01, Wednesday, Sunny",
"2021/12/10, Friday, Rainy",
"2021/12/13, Monday, Chilly"
})
public void test(@CsvToTestData TestData testData) {
System.out.println(testData.getDate() + " -- " + testData.getDayOfWeek() + " -- " + testData.getWeather());
}
作者:又语
原文:Spring Boot 单元测试(二)参数化测试 - 掘金