“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
, 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
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
is used by the @DisplayNameGeneration
annotation at class level. All tests within the class and its subclasses without a @DisplayName
annotation then use this DisplayNameGenerator
.
@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
There are no further rules for the composition of the names, so the IndicativeSentences
for test methods uses the class name and the method name, the ReplaceUnderscores
and the Standard
only use the method name. Our custom CamelCaseDisplayNameGenerator
should also use class and method names for generation.
The custom 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; @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
we use a dummy class DummyCamelCaseTest
and compare the generated text with our expectations "dummy camel case test"
.
In the test generateDisplayNameForNestedMethod
we use a dummy class DummyNestedTest
and the method dummyTestMethod
contained therein and compare the generated text with our expectations "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
. This parameter has access to the current test class and test method.
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
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
of the CamelCaseDisplayNameGenerator
is quickly implemented with a few code snippets from the FreshMarker library. The name of the class, without the unnecessary "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$", ""); } @Override public String generateDisplayNameForClass(Class<?> aClass) { return transform(removeTestSuffix(aClass.getSimpleName())); } // ... }
The second method 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); 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
first concatenates transformed class and method names and then appends the names of the parameters. The parameter list starts with the prefix " with "
and with the help of the EnumeratedCollector
from the stream-collector-utilities, the last parameter is appended with " and "
if there is more than one parameter.
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
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
on its own and another helpful method of the TestInfo
class.
@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
instance and previously generated by the CamelCaseDisplayNameGenerator
.