Better Tests Names Using JUnit's Display Name Generators
Writing unit tests can be challenging, but there is one thing that can get you on the right track — the test name.
Join the DZone community and get the full member experience.
Join For FreeWriting unit tests can be challenging, but there is one thing that can get you on the right track — the test name.
If you manage to give your test a good name — you will write a good test.
Unfortunately in some ( read: many) unit testing frameworks the test name must be a valid method name — because those "unit tests" actually function inside a class and so they look something like this:
public class CalculatorTests {
void add_passTwoPositiveNumbers_returnSum() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}
Test Names
In my tests, I've been using the naming scheme created by Roy Osherove. It forces you to think about the test before you write it and keep you from writing horrible unit tests.
The test names are built from three parts:
- The method running the test — this is the play button that would be used to execute the "experiment"
- The scenario we test — what is the system state before running the method, what input is used during the test — in other words, "what makes this test different from any other test ever written".
- The expected result — what we expect to happen when we run the method (1) with the specific state (2).
The good thing about using structured test names is that when a test fails we understand immediately what went wrong. Test names tell use what we're testing and what we expect to happen and together with the error message (from an assertion) we should quickly understand what went wrong, fix it, and have the code back to running smoothly in no time.
However — There Is a Problem
JUnit and it's successor xUnit testing frameworks use methods and classes to host the "tests" and so the test "name" must be a valid function name, and so I find myself using underscores and camel-case/pascal-case to help the reader of the method locate and understand the words I'm using.
It seems that in 2020 we're still haven't grasp the idea that test names do not have to be method names at least not in mainstream unit testing frameworks. I know some unit testing frameworks enable writing text as the test names but usually, when I get to a company they are using one of the popular unit testing frameworks which do not).
The JUnit5 Solution — Test Name Generators
JUnit5 did try and solve this issue, by adding the ability to mark your test with a test name generator:
xxxxxxxxxx
TestNameGenerator.class) (
public class CalculatorTests {
void add_passTwoPositiveNumbers_returnSum() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
The display name generators can be one of the pre-packaged name generators such as DisplayNameGenerator.ReplaceUnderscores that will automatically replace the underscores in your test names with spaces or you can write your own by extending one of the DisplayNameGenerator classes or by implementing the DisplayNameGenerator interface.
Then you can either use the @DisplayNameGenerator annotation on your test classes or methods or you can create a JUnit-platform.properties file and add the line:
junit.jupiter.displayname.generator.default =
<your DN generator>
My Solution
I've wanted to split the test names into the three parts and then add brackets around the method tested and have a test name that looks: (method): scenario -> expected result and so I wrote the following code:
xxxxxxxxxx
public class TestNameGenerator extends DisplayNameGenerator.Standard {
private String splitToParts(String input) {
try {
List<String> stringParts = getTestNameParts(input);
if (stringParts.size() == 1) {
return input;
}
if (stringParts.size() == 2) {
return String.format("(%s): Always %s", stringParts.get(0), stringParts.get(1));
}
if (stringParts.size() == 3) {
return String.format("(%s): %s -> %s", stringParts.get(0), stringParts.get(1), stringParts.get(2));
}
} catch (Exception exc) {
System.console().writer().println("Failed parsing test name");
}
return input;
}
private List<String> getTestNameParts(String input) {
List<String> stringParts = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); ++i) {
char ch = input.charAt(i);
if (ch == '(') {
break;
}
if (ch == '_') {
stringParts.add(sb.toString());
sb.setLength(0);
} else if (Character.isUpperCase(ch) && stringParts.size() > 0) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append(Character.toLowerCase(ch));
} else {
sb.append(ch);
}
}
stringParts.add(sb.toString());
return stringParts;
}
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
return splitToParts(
super.generateDisplayNameForMethod(testClass, testMethod)
);
}
}
It's quite a lot but it's basically replacing underscores ('_') with spaces (' ') and splitting to words based on upper case letters and also I've wanted to handle cases in which there are two parts to the test name.
Now when I run the tests I see the following results:
Which is exactly what I want, having readable test names helps me write better test names, it's hard to hide when it's written in plain English, and writing good test names helps me write better tests — but we've already covered that.
Happy coding...
Published at DZone with permission of Dror Helper, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments