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 使用 @ParameterizedTestname 属性

这是最简单的方式,直接指定格式模板即可。

@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 注解来定制测试类和方法的显示名称。我们还实现了两个自定义生成器:

  • 将驼峰命名转为可读格式
  • 生成具有上下文语义的描述性语句

此外,我们还探讨了在参数化测试中如何自定义参数的显示名称,包括:

  • 使用 @ParameterizedTestname 属性
  • 使用 Named 接口

这些技巧可以显著提升测试报告的可读性,特别是在大型测试套件中尤为重要。希望这些内容对你编写更清晰的单元测试有所帮助!

完整的示例代码可在 GitHub 上找到。


原始标题:JUnit Custom Display Name Generator API | Baeldung