Better tests names using JUnit’s display names generators

Writing 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” are actually functions inside a class and so they looks something like this:

public class CalculatorTests {

    @Test
    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:

  1. The method running the test – this is the play button that would be used to execute the “experiment”
  2. 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”.
  3. 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 immediatly what went wrong. Test names tells 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 the are using one of the popular unit testing framework which does not).

The JUnit5 solution – test name generators

JUnit5 did try and solve this issue, by adding thee ability to mark your test with a test name generator:

@DisplayNameGeneration(TestNameGenerator.class)
public class CalculatorTests {

    @Test
    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 you test names with spaces or you can write your own by extending one of the DisplayNameGenerator classes or by implementing DisplayNameGenerator interface.

Then you can either use the @DisplayNameGenerator annotation on your test classes or methods or you can create 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:

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;
    }

    @Override
    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 pain English and writing good test names helps me write better tests – but we’ve already covered that πŸ™‚

Happy coding…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.