1. 概述
JUnit 5 提供了良好的支持用于自定义测试类和测试方法的名称显示。在本教程中,我们将学习如何通过 @DisplayNameGeneration
注解使用 JUnit 5 的自定义显示名称生成器。
这对于提高测试报告的可读性非常有帮助,尤其是在使用嵌套测试类(@Nested
)或参数化测试时,可以更清晰地表达测试意图。
2. 显示名称生成机制
JUnit 5 允许我们通过 @DisplayNameGeneration
注解来配置自定义的显示名称生成器。需要注意的是:**@DisplayName
注解的优先级高于任何生成器**,也就是说,如果你为某个测试方法设置了 @DisplayName
,生成器将不会对它起作用。
JUnit 5 提供了一个默认的生成器类 DisplayNameGenerator.ReplaceUnderscores
,它会将方法名或类名中的下划线 _
替换为空格。看个例子:
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class ReplaceUnderscoresGeneratorUnitTest {
@Nested
class when_doing_something {
@Test
void then_something_should_happen() {
}
@Test
@DisplayName("@DisplayName takes precedence over generation")
void override_generator() {
}
}
}
运行后,测试输出如下:
└─ ReplaceUnderscoresGeneratorUnitTest ✓
└─ when doing something ✓
├─ then something should happen() ✓
└─ @DisplayName takes precedence over generation ✓
✅ 可以看到,下划线被替换成空格,提升了可读性。
3. 自定义显示名称生成器
要实现自定义显示名称生成器,我们需要实现 DisplayNameGenerator
接口,并重写其方法。该接口提供了三个方法用于生成类名、嵌套类名和方法名。
3.1 驼峰命名转可读句子
我们可以写一个生成器,把驼峰命名(如 camelCaseName
)转换为可读的句子(如 camel case name
)。
static class ReplaceCamelCase extends DisplayNameGenerator.Standard {
@Override
public String generateDisplayNameForClass(Class<?> testClass) {
return replaceCamelCase(super.generateDisplayNameForClass(testClass));
}
@Override
public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
return replaceCamelCase(super.generateDisplayNameForNestedClass(nestedClass));
}
@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
return this.replaceCamelCase(testMethod.getName()) +
DisplayNameGenerator.parameterTypesAsString(testMethod);
}
String replaceCamelCase(String camelCase) {
StringBuilder result = new StringBuilder();
result.append(camelCase.charAt(0));
for (int i=1; i<camelCase.length(); i++) {
if (Character.isUpperCase(camelCase.charAt(i))) {
result.append(' ');
result.append(Character.toLowerCase(camelCase.charAt(i)));
} else {
result.append(camelCase.charAt(i));
}
}
return result.toString();
}
}
测试代码如下:
@DisplayNameGeneration(DisplayNameGeneratorUnitTest.ReplaceCamelCase.class)
class DisplayNameGeneratorUnitTest {
@Test
void camelCaseName() {
}
}
运行结果:
└─ Display name generator unit test ✓
└─ camel case name() ✓
✅ 驼峰命名成功转换为可读格式。
3.2 生成描述性语句
我们可以更进一步,让生成器将嵌套类名和方法名组合成完整的句子,以表达更清晰的测试意图。
static class IndicativeSentences extends ReplaceCamelCase {
@Override
public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
return super.generateDisplayNameForNestedClass(nestedClass) + "...";
}
@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
return replaceCamelCase(testClass.getSimpleName() + " " + testMethod.getName()) + ".";
}
}
使用示例:
class DisplayNameGeneratorUnitTest {
@Nested
@DisplayNameGeneration(DisplayNameGeneratorUnitTest.IndicativeSentences.class)
class ANumberIsFizz {
@Test
void ifItIsDivisibleByThree() {
}
@ParameterizedTest(name = "Number {0} is fizz.")
@ValueSource(ints = { 3, 12, 18 })
void ifItIsOneOfTheFollowingNumbers(int number) {
}
}
@Nested
@DisplayNameGeneration(DisplayNameGeneratorUnitTest.IndicativeSentences.class)
class ANumberIsBuzz {
@Test
void ifItIsDivisibleByFive() {
}
@ParameterizedTest(name = "Number {0} is buzz.")
@ValueSource(ints = { 5, 10, 20 })
void ifItIsOneOfTheFollowingNumbers(int number) {
}
}
}
运行结果:
└─ Display name generator unit test ✓
├─ A number is buzz... ✓
│ ├─ A number is buzz if it is one of the following numbers. ✓
│ │ ├─ Number 5 is buzz. ✓
│ │ ├─ Number 10 is buzz. ✓
│ │ └─ Number 20 is buzz. ✓
│ └─ A number is buzz if it is divisible by five. ✓
└─ A number is fizz... ✓
├─ A number is fizz if it is one of the following numbers. ✓
│ ├─ Number 3 is fizz. ✓
│ ├─ Number 12 is fizz. ✓
│ └─ Number 18 is fizz. ✓
└─ A number is fizz if it is divisible by three. ✓
✅ 这样一来,测试的上下文关系更清晰,便于理解测试逻辑。
4. 参数化测试中的参数名称自定义
在参数化测试中,JUnit 默认会为每个参数生成一个带索引的字符串,例如 [1] City: Madrid
。我们可以通过以下方式自定义这些名称。
4.1 使用 @ParameterizedTest
的 name
属性
这是最简单的方式,直接指定格式模板即可。
@ParameterizedTest(name = "Parameter with index {index} => {0}")
@MethodSource("argumentsProvider")
void whenUsingNameAttribute_thenGenerateCustomDisplayNames(String givenArg) {
// Test
}
运行结果:
└─ whenUsingNameAttribute_thenGenerateCustomDisplayNames
├─ Parameter with index 1 => City: Madrid
├─ Parameter with index 2 => Country: Spain
├─ Parameter with index 3 => Continent: Europe
✅ 模板语法简单,适用于大多数场景。
4.2 使用 Named
接口
如果你希望为每个参数单独指定一个名称,可以使用 Named.of()
方法。
@ParameterizedTest
@MethodSource("namedArguments")
void whenUsingNamedInterface_thenGenerateCustomDisplayNames(String givenArg) {
// Test
}
private static Stream<Arguments> namedArguments() {
return Stream.of(
Arguments.of(Named.of("Testing with a city", "Tokyo")),
Arguments.of(Named.of("Testing with a country", "Japan")),
Arguments.of(Named.of("Testing with a continent", "Asia")));
}
运行结果:
└─ whenUsingNamedInterface_thenGenerateCustomDisplayNames
├─ [1] Testing with a city
├─ [2] Testing with a country
├─ [3] Testing with a continent
✅ 这种方式更灵活,适用于需要精确控制每个参数显示名称的场景。
5. 总结
本文介绍了如何使用 JUnit 5 的 @DisplayNameGeneration
注解来定制测试类和方法的显示名称。我们还实现了两个自定义生成器:
- 将驼峰命名转为可读格式
- 生成具有上下文语义的描述性语句
此外,我们还探讨了在参数化测试中如何自定义参数的显示名称,包括:
- 使用
@ParameterizedTest
的name
属性 - 使用
Named
接口
这些技巧可以显著提升测试报告的可读性,特别是在大型测试套件中尤为重要。希望这些内容对你编写更清晰的单元测试有所帮助!
完整的示例代码可在 GitHub 上找到。