3、参数化测试
一、什么是参数化测试?
参数化测试允许使用不同的输入参数多次运行同一个测试方法,从而减少重复代码并提高测试覆盖率。它通过 @ParameterizedTest
注解标记测试方法,并通过指定参数来源(如值列表、CSV、方法等)提供输入数据。
核心特点:
- 多数据输入:同一测试逻辑用不同参数重复执行。
- 灵活的参数来源:支持从简单值、CSV、方法、枚举等多种方式获取参数。
- 可定制显示名称:动态生成测试用例的描述。
二、参数化测试的核心注解和组件
注解/类 | 说明 |
---|---|
@ParameterizedTest | 标记参数化测试方法 |
@ValueSource | 提供基本类型(如 int、String)的参数列表 |
@CsvSource /@CsvFileSource | 从 CSV 格式的字符串或文件读取参数 |
@MethodSource | 从静态方法获取参数 |
@EnumSource | 从枚举类型获取参数 |
@ArgumentsSource | 自定义参数源(需实现 ArgumentsProvider 接口) |
@NullSource /@EmptySource | 提供 null 或空值参数(如空字符串、集合) |
@DisplayName | 自定义测试名称(支持占位符如 {0} 、{1} ) |
三、使用场景
- 多参数组合测试
例如测试加法运算的不同输入组合。 - 边界值测试
验证输入值在边界条件下的行为(如最小值、最大值)。 - 数据驱动测试
从外部文件(如 CSV、JSON)加载测试数据。 - 枚举值遍历测试
验证枚举类型的所有可能值。
四、参数化测试的使用步骤
1. 添加依赖
确保 junit-jupiter-params
已添加到项目中:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
2. 编写参数化测试方法
使用 @ParameterizedTest
和参数来源注解。
五、参数来源示例
示例 1:@ValueSource
(基本类型)
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, 7})
void testOddNumbers(int number) {
Assertions.assertTrue(number % 2 != 0);
}
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "madam"})
void testPalindromes(String word) {
Assertions.assertTrue(isPalindrome(word));
}
示例 2:@CsvSource
(多参数)
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"5, 5, 10",
"10, -5, 5"
})
void testAddition(int a, int b, int expected) {
Assertions.assertEquals(expected, a + b);
}
示例 3:@CsvFileSource
(从 CSV 文件读取)
创建文件 test-data.csv
:
a,b,expected
2,3,5
5,5,10
10,-5,5
测试代码:
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testAdditionFromCsv(int a, int b, int expected) {
Assertions.assertEquals(expected, a + b);
}
示例 4:@MethodSource
(从方法获取参数)
@ParameterizedTest
@MethodSource("provideStringsForTest")
void testStringLength(String input, int expectedLength) {
Assertions.assertEquals(expectedLength, input.length());
}
// 提供参数的静态方法(方法名默认与测试方法名匹配)
private static Stream<Arguments> provideStringsForTest() {
return Stream.of(
Arguments.of("hello", 5),
Arguments.of("world", 5),
Arguments.of("", 0)
);
}
示例 5:@EnumSource
(枚举参数)
enum Day { MONDAY, TUESDAY, WEDNESDAY }
@ParameterizedTest
@EnumSource(Day.class)
void testEnumValues(Day day) {
Assertions.assertNotNull(day);
}
示例 6:@NullSource
和 @EmptySource
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = {" ", " "})
void testNullOrEmptyStrings(String input) {
Assertions.assertTrue(input == null || input.trim().isEmpty());
}
六、自定义参数显示名称
通过 name
属性自定义测试名称(支持占位符):
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"2, 3, 5",
"5, 5, 10"
})
void testAdditionWithCustomName(int a, int b, int expected) {
Assertions.assertEquals(expected, a + b);
}
输出结果:
√ 2 + 3 = 5
√ 5 + 5 = 10
七、参数类型转换
JUnit 5 支持自动类型转换(如字符串转日期):
@ParameterizedTest
@CsvSource({"2023-01-01, 2023-12-31"})
void testDateRange(LocalDate start, LocalDate end) {
Assertions.assertTrue(start.isBefore(end));
}
自定义转换器
实现 ArgumentConverter
接口处理复杂类型:
@ParameterizedTest
@CsvSource({"apple, 5", "banana, 6"})
void testCustomConverter(@ConvertWith(FruitConverter.class) Fruit fruit, int length) {
Assertions.assertEquals(length, fruit.getName().length());
}
static class FruitConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
return new Fruit(((String) source).toUpperCase());
}
}
static class Fruit {
private final String name;
Fruit(String name) { this.name = name; }
public String getName() { return name; }
}
八、参数化测试的生命周期
- 支持生命周期方法:与普通
@Test
方法一样,参数化测试会触发@BeforeEach
和@AfterEach
。 - 每个参数独立执行:每个参数组合会作为独立的测试用例运行。
九、参数化测试 vs 动态测试
特性 | 参数化测试 | 动态测试 |
---|---|---|
参数来源 | 预定义参数源(如 CSV、方法) | 动态生成(运行时逻辑) |
用例数量 | 固定(编译时确定) | 动态(运行时确定) |
生命周期 | 支持 @BeforeEach /@AfterEach | 不支持 |
适用场景 | 固定参数组合 | 复杂逻辑生成用例 |
十、总结
参数化测试在以下场景中非常有用:
- 需要验证多组输入数据的行为。
- 避免编写重复的测试代码。
- 数据驱动测试(如从文件或数据库加载数据)。
通过灵活的参数来源和类型转换,可以覆盖广泛的测试需求。对于固定参数组合,优先选择参数化测试;对于运行时动态生成的用例,选择动态测试。