1. 概述
JUnit 5 是 JUnit 的新一代版本,带来了许多现代化的特性,其中一项非常实用的功能就是 参数化测试(Parameterized Tests)。它允许我们使用不同的参数多次运行同一个测试方法。
本文将深入探讨 JUnit 5 中参数化测试的使用方式,帮助你写出更灵活、更简洁的测试代码。
2. 依赖配置
要使用 JUnit 5 的参数化测试功能,我们需要引入 junit-jupiter-params
模块。在 Maven 项目中,添加如下依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.11.0-M2</version>
<scope>test</scope>
</dependency>
如果使用 Gradle,则配置如下:
testCompile("org.junit.jupiter:junit-jupiter-params:5.10.0")
3. 初识参数化测试
假设我们有一个工具类方法,需要验证它的行为是否符合预期:
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
要对这个方法进行参数化测试,只需添加 @ParameterizedTest
注解:
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // 六个数字
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
JUnit 5 测试运行器会执行这个测试方法六次,每次传入不同的参数值。
这个例子展示了参数化测试的两个关键点:
✅ 一个参数来源(这里是 int
数组)
✅ 一个接收参数的方式(这里是 number
参数)
不过这只是冰山一角,继续往下看。
4. 参数来源详解
参数化测试的核心在于 参数来源(Argument Sources),JUnit 5 提供了多种方式来定义这些来源。
4.1. 简单值:@ValueSource
通过 @ValueSource
注解,我们可以传入一组字面量值:
public class Strings {
public static boolean isBlank(String input) {
return input == null || input.trim().isEmpty();
}
}
测试代码如下:
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
⚠️ 注意事项:
@ValueSource
只支持基本类型和String
、Class
等少数类型- 每次只能传入一个参数
- 不能传
null
,即使是String
类型也不行
4.2. 空值与空字符串:@NullSource、@EmptySource、@NullAndEmptySource
从 JUnit 5.4 开始,可以使用以下注解传递 null
或空值:
@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
assertTrue(Strings.isBlank(input));
}
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
还可以组合使用:
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
4.3. 枚举值:@EnumSource
要使用枚举值进行测试,可以使用 @EnumSource
:
@ParameterizedTest
@EnumSource(Month.class)
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
还可以通过 names
属性筛选特定枚举值:
@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
支持正向匹配、反向排除、正则匹配等模式:
@ParameterizedTest
@EnumSource(
value = Month.class,
names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(31, month.length(isALeapYear));
}
4.4. CSV 字面量:@CsvSource
当需要传递多个参数时,可以使用 @CsvSource
:
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
支持自定义分隔符:
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
String actualValue = input.toLowerCase();
assertEquals(expected, actualValue);
}
4.5. CSV 文件:@CsvFileSource
也可以从 CSV 文件中读取参数:
input,expected
test,TEST
tEst,TEST
Java,JAVA
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
支持跳过行、自定义分隔符、编码等配置。
4.6. 方法来源:@MethodSource
对于复杂对象,可以使用 @MethodSource
指定一个方法作为参数来源:
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
如果方法名与测试方法名一致,可以省略参数:
@ParameterizedTest
@MethodSource
void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument(String input) {
assertTrue(Strings.isBlank(input));
}
private static Stream<String> isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument() {
return Stream.of(null, "", " ");
}
还可以引用外部类的方法:
@ParameterizedTest
@MethodSource("com.baeldung.parameterized.StringParams#blankStrings")
void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {
assertTrue(Strings.isBlank(input));
}
4.7. 字段来源:@FieldSource(JUnit 5.11+)
从 JUnit 5.11 开始,支持使用静态字段作为参数来源:
static List<String> cities = Arrays.asList("Madrid", "Rome", "Paris", "London");
@ParameterizedTest
@FieldSource("cities")
void isBlank_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter(String arg) {
assertFalse(Strings.isBlank(arg));
}
字段类型可以是 Collection
、Iterable
、数组或 Stream
。
4.8. 自定义参数提供器:@ArgumentsSource
可以通过实现 ArgumentsProvider
接口自定义参数来源:
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
然后使用:
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
4.9. 自定义注解
可以将自定义提供器封装成注解,提升可读性:
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {
String value();
}
配合自定义的 VariableArgumentsProvider
实现,即可使用:
@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
5. 可重复的参数源注解(JUnit 5.11+)
从 JUnit 5.11 开始,大多数参数源注解支持重复使用:
@ParameterizedTest
@MethodSource("asia")
@MethodSource("europe")
void whenStringIsLargerThanThreeCharacters_thenReturnTrue(String country) {
assertTrue(country.length() > 3);
}
支持重复使用的注解包括:
@ValueSource
@EnumSource
@MethodSource
@FieldSource
@CsvSource
@CsvFileSource
@ArgumentsSource
6. 参数转换
6.1. 隐式转换
JUnit 5 提供了多种内置的隐式类型转换,例如:
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
支持的类型包括:
UUID
Locale
LocalDate
,LocalTime
,LocalDateTime
,Year
,Month
File
,Path
URL
,URI
- 枚举子类
6.2. 显式转换
可以通过实现 ArgumentConverter
接口来自定义转换逻辑:
class SlashyDateConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
if (!(source instanceof String)) {
throw new IllegalArgumentException("The argument should be a string: " + source);
}
try {
String[] parts = ((String) source).split("/");
int year = Integer.parseInt(parts[0]);
int month = Integer.parseInt(parts[1]);
int day = Integer.parseInt(parts[2]);
return LocalDate.of(year, month, day);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert", e);
}
}
}
然后使用 @ConvertWith
注解引用:
@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
assertEquals(expected, date.getYear());
}
7. 参数访问器:ArgumentsAccessor
当参数较多时,可以通过 ArgumentsAccessor
来简化方法签名:
@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {
String firstName = argumentsAccessor.getString(0);
String middleName = (String) argumentsAccessor.get(1);
String lastName = argumentsAccessor.get(2, String.class);
String expectedFullName = argumentsAccessor.getString(3);
Person person = new Person(firstName, middleName, lastName);
assertEquals(expectedFullName, person.fullName());
}
8. 参数聚合器:ArgumentsAggregator
为了提高代码复用性,可以使用 ArgumentsAggregator
将多个参数聚合为一个对象:
class PersonAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
return new Person(
accessor.getString(1), accessor.getString(2), accessor.getString(3));
}
}
使用方式如下:
@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(
String expectedFullName,
@AggregateWith(PersonAggregator.class) Person person) {
assertEquals(expectedFullName, person.fullName());
}
9. 自定义显示名称
默认情况下,参数化测试的显示名称会包含索引和参数内容:
├─ someMonths_Are30DaysLongCsv(Month)
│ │ ├─ [1] APRIL
│ │ ├─ [2] JUNE
│ │ ├─ [3] SEPTEMBER
│ │ └─ [4] NOVEMBER
可以通过 name
属性自定义:
@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
显示效果:
├─ someMonths_Are30DaysLong(Month)
│ │ ├─ 1 APRIL is 30 days long
│ │ ├─ 2 JUNE is 30 days long
│ │ ├─ 3 SEPTEMBER is 30 days long
│ │ └─ 4 NOVEMBER is 30 days long
支持的占位符:
{index}
:测试索引(从 1 开始){arguments}
:完整参数列表{0}, {1}, ...
:单个参数
10. 总结
JUnit 5 的参数化测试功能非常强大,能够显著减少重复代码,提高测试覆盖率。通过灵活使用参数源、参数转换和自定义注解,可以写出更加简洁、易维护的测试代码。
完整示例代码可在 GitHub 上找到。