Better names for JUnit 5 Test

“All things are defined by names. Change the name, and you change the thing.”

Terry Pratchett

The JUnit 5 test library offers a wide range of options for extending the framework and thus simplifying the work of software developers. One of these options is the

DisplayNameGenerator
DisplayNameGenerator, which can be used to modify the generation of test names in the console output. When creating exercises for a course, I was surprised to find that there is a
DisplayNameGenerator
DisplayNameGenerator that replaces underscores with spaces, but none that handles the Camel Case form. Fortunately, this can be changed.

It is somewhat strange that only the Snake Case form is supported, as Camel Case has been the Java Convention for class, method and variable names since ever. Only constants are written in the Screaming Snake Case form. The only reason for the sporadic occurrence of underscores in method names is then often defended with better readability. With 99% Camel Case notation in the rest of the code and all the libraries used, this is a somewhat simple excuse.

The

DisplayNameGenerator
DisplayNameGenerator is used by the
@DisplayNameGeneration
@DisplayNameGeneration annotation at class level. All tests within the class and its subclasses without a
@DisplayName
@DisplayName annotation then use this
DisplayNameGenerator
DisplayNameGenerator.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class SnakeCaseDisplayNameTest {
@Test
void generate_display_name_for_class() {
}
}
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SnakeCaseDisplayNameTest { @Test void generate_display_name_for_class() { } }
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class SnakeCaseDisplayNameTest {

    @Test
    void generate_display_name_for_class() {
    }
}

The test in the example, with the non-Java-compliant name, generates the following output for its name.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
generate display name for class
generate display name for class
generate display name for class

There are no further rules for the composition of the names, so the

IndicativeSentences
IndicativeSentences for test methods uses the class name and the method name, the
ReplaceUnderscores
ReplaceUnderscores and the
Standard
Standard only use the method name. Our custom
CamelCaseDisplayNameGenerator
CamelCaseDisplayNameGenerator should also use class and method names for generation.

The custom

DisplayNameGenerator
DisplayNameGenerator must implement three methods. The first generates a name for a top level class, the second generates the name for a nested class and the third generates a name for a method.

Before the implementation begins, we write a few tests that our implementation should fulfil. Two of them are shown here as examples.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class CamelCaseDisplayNameGeneratorTest {
private CamelCaseDisplayNameGenerator generator;
@BeforeEach
void setUp() {
generator = new CamelCaseDisplayNameGenerator();
}
@Test
void generateDisplayNameForClass() {
Class<?> testClass = DummyCamelCaseTest.class;
assertEquals("dummy camel case test", generator.generateDisplayNameForClass(testClass));
}
@Test
void generateDisplayNameForNestedMethod() {
Class<?> testClass = DummyNestedTest.class;
Method testMethod = getMethod(testClass);
assertEquals("dummy camel case test dummy nested test: dummy test method", generator.generateDisplayNameForMethod(testClass, testMethod));
}
}
class CamelCaseDisplayNameGeneratorTest { private CamelCaseDisplayNameGenerator generator; @BeforeEach void setUp() { generator = new CamelCaseDisplayNameGenerator(); } @Test void generateDisplayNameForClass() { Class<?> testClass = DummyCamelCaseTest.class; assertEquals("dummy camel case test", generator.generateDisplayNameForClass(testClass)); } @Test void generateDisplayNameForNestedMethod() { Class<?> testClass = DummyNestedTest.class; Method testMethod = getMethod(testClass); assertEquals("dummy camel case test dummy nested test: dummy test method", generator.generateDisplayNameForMethod(testClass, testMethod)); } }
class CamelCaseDisplayNameGeneratorTest {

  private CamelCaseDisplayNameGenerator generator;

  @BeforeEach
  void setUp() {
    generator = new CamelCaseDisplayNameGenerator();
  }

  @Test
  void generateDisplayNameForClass() {
    Class<?> testClass = DummyCamelCaseTest.class;
    assertEquals("dummy camel case test", generator.generateDisplayNameForClass(testClass));
  }

  @Test
  void generateDisplayNameForNestedMethod() {
    Class<?> testClass = DummyNestedTest.class;
    Method testMethod = getMethod(testClass);
    assertEquals("dummy camel case test dummy nested test: dummy test method", generator.generateDisplayNameForMethod(testClass, testMethod));
  }
}

In the test

generateDisplayNameForClass
generateDisplayNameForClass we use a dummy class
DummyCamelCaseTest
DummyCamelCaseTest and compare the generated text with our expectations
"dummy camel case test"
"dummy camel case test".

In the test

generateDisplayNameForNestedMethod
generateDisplayNameForNestedMethod we use a dummy class
DummyNestedTest
DummyNestedTest and the method
dummyTestMethod
dummyTestMethod contained therein and compare the generated text with our expectations
"dummy camel case test dummy nested test: dummy test method"
"dummy camel case test dummy nested test: dummy test method".

There are two things I don’t like about this test: the additional dummy classes and the omitted access to the dummy method by reflections. Both can be elegantly corrected with a JUnit 5 feature. All test methods can have a parameter of type

TestInfo
TestInfo. This parameter has access to the current test class and test method.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class CamelCaseDisplayNameGeneratorTest {
private CamelCaseDisplayNameGenerator generator;
@BeforeEach
void setUp() {
generator = new CamelCaseDisplayNameGenerator();
}
@Test
void generateDisplayNameForClass(TestInfo testInfo) {
Class<?> testClass = testInfo.getTestClass().orElseThrow();
assertEquals("camel case display name generator", generator.generateDisplayNameForClass(testClass));
}
@Nested
class ForNestedClasses {
@Test
void generateDisplayNameForMethod(TestInfo testInfo) {
Class<?> testClass = testInfo.getTestClass().orElseThrow();
Method testMethod = testInfo.getTestMethod().orElseThrow();
assertEquals("camel case display name generator for nested classes: generate display name for method with test info", generator.generateDisplayNameForMethod(testClass, testMethod));
}
}
}
class CamelCaseDisplayNameGeneratorTest { private CamelCaseDisplayNameGenerator generator; @BeforeEach void setUp() { generator = new CamelCaseDisplayNameGenerator(); } @Test void generateDisplayNameForClass(TestInfo testInfo) { Class<?> testClass = testInfo.getTestClass().orElseThrow(); assertEquals("camel case display name generator", generator.generateDisplayNameForClass(testClass)); } @Nested class ForNestedClasses { @Test void generateDisplayNameForMethod(TestInfo testInfo) { Class<?> testClass = testInfo.getTestClass().orElseThrow(); Method testMethod = testInfo.getTestMethod().orElseThrow(); assertEquals("camel case display name generator for nested classes: generate display name for method with test info", generator.generateDisplayNameForMethod(testClass, testMethod)); } } }
class CamelCaseDisplayNameGeneratorTest {

    private CamelCaseDisplayNameGenerator generator;

    @BeforeEach
    void setUp() {
        generator = new CamelCaseDisplayNameGenerator();
    }

    @Test
    void generateDisplayNameForClass(TestInfo testInfo) {
        Class<?> testClass = testInfo.getTestClass().orElseThrow();
        assertEquals("camel case display name generator", generator.generateDisplayNameForClass(testClass));
    }

    @Nested
    class ForNestedClasses {
        @Test
        void generateDisplayNameForMethod(TestInfo testInfo) {
            Class<?> testClass = testInfo.getTestClass().orElseThrow();
            Method testMethod = testInfo.getTestMethod().orElseThrow();
            assertEquals("camel case display name generator for nested classes: generate display name for method with test info", generator.generateDisplayNameForMethod(testClass, testMethod));
        }
    }
}

The two tests with

TestInfo
TestInfo parameters now test their own names. In fact, the tests could now be written even shorter, but we’ll wait until the end of this article. The only drawback with this solution is the fact that test methods without parameters cannot be tested in this way. Of course, because they all have one parameter.

The first method

generateDisplayNameForClass
generateDisplayNameForClass of the
CamelCaseDisplayNameGenerator
CamelCaseDisplayNameGenerator is quickly implemented with a few code snippets from the FreshMarker library. The name of the class, without the unnecessary
"Test"
"Test" suffix, is passed to a regular expression that recognises Camel Case separators.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
private static final String LOWER_CASE_UPPER_CASES = "(\\p{javaLowerCase})(\\p{javaUpperCase}+)";
private String transform(String input) {
return input.replaceAll(LOWER_CASE_UPPER_CASES, "$1 $2").toLowerCase();
}
private String removeTestSuffix(String input) {
return input.replaceFirst("Test$", "");
}
@Override
public String generateDisplayNameForClass(Class<?> aClass) {
return transform(removeTestSuffix(aClass.getSimpleName()));
}
// ...
}
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator { private static final String LOWER_CASE_UPPER_CASES = "(\\p{javaLowerCase})(\\p{javaUpperCase}+)"; private String transform(String input) { return input.replaceAll(LOWER_CASE_UPPER_CASES, "$1 $2").toLowerCase(); } private String removeTestSuffix(String input) { return input.replaceFirst("Test$", ""); } @Override public String generateDisplayNameForClass(Class<?> aClass) { return transform(removeTestSuffix(aClass.getSimpleName())); } // ... }
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
  private static final String LOWER_CASE_UPPER_CASES = "(\\p{javaLowerCase})(\\p{javaUpperCase}+)";

  private String transform(String input) {
    return input.replaceAll(LOWER_CASE_UPPER_CASES, "$1 $2").toLowerCase();
  }

  private String removeTestSuffix(String input) {
    return input.replaceFirst("Test$", "");
  }

  @Override
  public String generateDisplayNameForClass(Class<?> aClass) {
    return transform(removeTestSuffix(aClass.getSimpleName()));
  }
  // ...
}

The second method

generateDisplayNameForNestedClass
generateDisplayNameForNestedClass first collects all classes in the hierarchy and then concatenates their transformed names.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
//...
private List<Class<?>> enclosingClasses(Class<?> aClass) {
List<Class<?>> result = new ArrayList<>();
Class<?> currentClass = aClass;
result.add(currentClass);
do {
currentClass = currentClass.getEnclosingClass();
result.add(currentClass);
} while (currentClass.isMemberClass());
return result.reversed();
}
@Override
public String generateDisplayNameForNestedClass(Class<?> aClass) {
return enclosingClasses(aClass).stream().map(Class::getSimpleName)
.map(this::removeTestSuffix).map(this::transform)
.collect(joining(" "));
}
//...
}
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator { //... private List<Class<?>> enclosingClasses(Class<?> aClass) { List<Class<?>> result = new ArrayList<>(); Class<?> currentClass = aClass; result.add(currentClass); do { currentClass = currentClass.getEnclosingClass(); result.add(currentClass); } while (currentClass.isMemberClass()); return result.reversed(); } @Override public String generateDisplayNameForNestedClass(Class<?> aClass) { return enclosingClasses(aClass).stream().map(Class::getSimpleName) .map(this::removeTestSuffix).map(this::transform) .collect(joining(" ")); } //... }
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
  //...
  private List<Class<?>> enclosingClasses(Class<?> aClass) {
    List<Class<?>> result = new ArrayList<>();
    Class<?> currentClass = aClass;
    result.add(currentClass);
    do {
      currentClass = currentClass.getEnclosingClass();
      result.add(currentClass);
    } while (currentClass.isMemberClass());
    return result.reversed();
  }

  @Override
  public String generateDisplayNameForNestedClass(Class<?> aClass) {
    return enclosingClasses(aClass).stream().map(Class::getSimpleName)
      .map(this::removeTestSuffix).map(this::transform)
      .collect(joining(" "));
  }
  //...
}

The third method

generateDisplayNameForMethod
generateDisplayNameForMethod first concatenates transformed class and method names and then appends the names of the parameters. The parameter list starts with the prefix
" with "
" with " and with the help of the
EnumeratedCollector
EnumeratedCollector from the stream-collector-utilities, the last parameter is appended with
" and "
" and " if there is more than one parameter.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
//...
@Override
public String generateDisplayNameForMethod(Class<?> aClass, Method method) {
String displayNameForClass = aClass.isMemberClass() ? generateDisplayNameForNestedClass(aClass) : generateDisplayNameForClass(aClass);
String parameters = Stream.of(method.getParameters()).map(Parameter::getName).collect(EnumeratedCollector.enumerated(" and "));
return displayNameForClass + ": " + transform(method.getName() + " with " + parameters);
}
}
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator { //... @Override public String generateDisplayNameForMethod(Class<?> aClass, Method method) { String displayNameForClass = aClass.isMemberClass() ? generateDisplayNameForNestedClass(aClass) : generateDisplayNameForClass(aClass); String parameters = Stream.of(method.getParameters()).map(Parameter::getName).collect(EnumeratedCollector.enumerated(" and ")); return displayNameForClass + ": " + transform(method.getName() + " with " + parameters); } }
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
  //...
  @Override
  public String generateDisplayNameForMethod(Class<?> aClass, Method method) {
    String displayNameForClass = aClass.isMemberClass() ? generateDisplayNameForNestedClass(aClass) : generateDisplayNameForClass(aClass);
    String parameters = Stream.of(method.getParameters()).map(Parameter::getName).collect(EnumeratedCollector.enumerated(" and "));
    return displayNameForClass + ": " + transform(method.getName() + " with " + parameters);
  }
}

The

CamelCaseDisplayNameGenerator
CamelCaseDisplayNameGenerator is now implemented and can be used. If you’re still interested in the even more compact fest class at the end, then hopefully you like bootstrapping. The test class uses the
CamelCaseDisplayNameGenerator
CamelCaseDisplayNameGenerator on its own and another helpful method of the
TestInfo
TestInfo class.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@DisplayNameGeneration(CamelCaseDisplayNameGenerator.class)
@ExtendWith(SampleExtension.class)
class CamelCaseDisplayNameGeneratorTest {
@Nested
class ForNestedClasses {
@Test
void generateDisplayNameForMethod(TestInfo testInfo) {
assertEquals("camel case display name generator for nested classes: generate display name for method with test info", testInfo.getDisplayName());
}
@Test
void generateDisplayNameForMethod(TestInfo testInfo, String value) {
assertEquals("camel case display name generator for nested classes: generate display name for method with test info and value", testInfo.getDisplayName());
}
}
@Test
void generateDisplayNameForMethod(TestInfo testInfo) {
assertEquals("camel case display name generator: generate display name for method with test info", testInfo.getDisplayName());
}
@Test
void generateDisplayNameForMethod(TestInfo testInfo, String value) {
assertEquals("camel case display name generator: generate display name for method with test info and value", testInfo.getDisplayName());
}
}
@DisplayNameGeneration(CamelCaseDisplayNameGenerator.class) @ExtendWith(SampleExtension.class) class CamelCaseDisplayNameGeneratorTest { @Nested class ForNestedClasses { @Test void generateDisplayNameForMethod(TestInfo testInfo) { assertEquals("camel case display name generator for nested classes: generate display name for method with test info", testInfo.getDisplayName()); } @Test void generateDisplayNameForMethod(TestInfo testInfo, String value) { assertEquals("camel case display name generator for nested classes: generate display name for method with test info and value", testInfo.getDisplayName()); } } @Test void generateDisplayNameForMethod(TestInfo testInfo) { assertEquals("camel case display name generator: generate display name for method with test info", testInfo.getDisplayName()); } @Test void generateDisplayNameForMethod(TestInfo testInfo, String value) { assertEquals("camel case display name generator: generate display name for method with test info and value", testInfo.getDisplayName()); } }
@DisplayNameGeneration(CamelCaseDisplayNameGenerator.class)
@ExtendWith(SampleExtension.class)
class CamelCaseDisplayNameGeneratorTest {

    @Nested
    class ForNestedClasses {
        @Test
        void generateDisplayNameForMethod(TestInfo testInfo) {
            assertEquals("camel case display name generator for nested classes: generate display name for method with test info", testInfo.getDisplayName());
        }

        @Test
        void generateDisplayNameForMethod(TestInfo testInfo, String value) {
            assertEquals("camel case display name generator for nested classes: generate display name for method with test info and value", testInfo.getDisplayName());
        }
    }

    @Test
    void generateDisplayNameForMethod(TestInfo testInfo) {
        assertEquals("camel case display name generator: generate display name for method with test info", testInfo.getDisplayName());
    }

    @Test
    void generateDisplayNameForMethod(TestInfo testInfo, String value) {
        assertEquals("camel case display name generator: generate display name for method with test info and value", testInfo.getDisplayName());
    }
}

The test methods check their name, which was passed to them by the

TestInfo
TestInfo instance and previously generated by the
CamelCaseDisplayNameGenerator
CamelCaseDisplayNameGenerator.

Leave a Comment