“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
.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class SnakeCaseDisplayNameTest {
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.
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.
class CamelCaseDisplayNameGeneratorTest {
private CamelCaseDisplayNameGenerator generator;
generator = new CamelCaseDisplayNameGenerator();
void generateDisplayNameForClass() {
Class<?> testClass = DummyCamelCaseTest.class;
assertEquals("dummy camel case test", generator.generateDisplayNameForClass(testClass));
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.
class CamelCaseDisplayNameGeneratorTest {
private CamelCaseDisplayNameGenerator generator;
generator = new CamelCaseDisplayNameGenerator();
void generateDisplayNameForClass(TestInfo testInfo) {
Class<?> testClass = testInfo.getTestClass().orElseThrow();
assertEquals("camel case display name generator", generator.generateDisplayNameForClass(testClass));
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.
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$", "");
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.
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
private List<Class<?>> enclosingClasses(Class<?> aClass) {
List<Class<?>> result = new ArrayList<>();
Class<?> currentClass = aClass;
result.add(currentClass);
currentClass = currentClass.getEnclosingClass();
result.add(currentClass);
} while (currentClass.isMemberClass());
return result.reversed();
public String generateDisplayNameForNestedClass(Class<?> aClass) {
return enclosingClasses(aClass).stream().map(Class::getSimpleName)
.map(this::removeTestSuffix).map(this::transform)
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.
public class CamelCaseDisplayNameGenerator implements DisplayNameGenerator {
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.
@DisplayNameGeneration(CamelCaseDisplayNameGenerator.class)
@ExtendWith(SampleExtension.class)
class CamelCaseDisplayNameGeneratorTest {
void generateDisplayNameForMethod(TestInfo testInfo) {
assertEquals("camel case display name generator for nested classes: generate display name for method with test info", testInfo.getDisplayName());
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());
void generateDisplayNameForMethod(TestInfo testInfo) {
assertEquals("camel case display name generator: generate display name for method with test info", testInfo.getDisplayName());
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
.