Use @DisplayName and @Nested to Document and Structure JUnit Tests

Best Practice

Keep test code better maintainable by documenting and structuring tests with JUnit-provided annotations.

Keeping code maintainable is one of the biggest challenges in software engineering. This is true not only for production code but also for tests. Apart from basics like focussing on one single aspect and not trying to test everything in one test case, it is advisable to structure and document tests in a way, that developers have a chance to understand painlessly the intention and are able to adjust tests as needed when e.g. the code under test has changed. One way of achieving this is by using the JUnit-provided annotations @DisplayName (official docs) and @Nested (official docs).

The subject under test for the following examples is a simple list holding attachments:

 1public class AttachmentList extends ArrayList<Attachment> {
 2    public boolean add(final Attachment attachment) {
 3        Objects.requireNonNull(attachment);
 5        if (attachment.getSize() <= 0) {
 6            throw new IllegalArgumentException("Only attachments with sizes > 0 allowed");
 7        }
 9        return super.add(attachment);
10    }

By default, all tests in a test class are structured in a flat way and e.g. Intellij simply prints the method names for each test case and the result when executing a test suite. Many developers try to express the intention of a test through the test method name and are used to the times when it was mandatory to prefix all test cases with test which leads to method names like testNpeIsThrownWhenAddingNullAttachment or even worse test_npe_is_thrown_when_adding_null_attachment. This is problematic in several ways:

  • Long method names should be avoided when possible, it makes the code harder to read; as always in software engineering: Keep it simple.
  • Proper sentences with real grammar and punctuation are easier to read and understand; we are humans, not machines.
  • Underscores in method names are against the Java Code Conventions and a No-Go.

With the legacy approach (omitting the underscores), the test execution in Intellij looks like this:

Test execution in IntelliJ the legacy way

The intention of the individual test cases is more or less clear, but it is not easy to read. The better approach is to keep the test method name short, simple and tidy and to use @DisplayName to express and document the intention of a test case like

2@DisplayName("should throw a NPE when trying to add null")
3void nullObject() {
4    assertThrows(NullPointerException.class, () -> attachmentList.add(null));

Simply by using @DisplayName annotations on all test methods and the test class itself the text execution looks much cleaner:

Test execution in IntelliJ with @DisplayName annotated test methods and class

When a test case fails, it is clear at a glance what was tested and failed. Still, there is no inner structure, which isn't a problem in this small example, but often test classes contain many tests targeting different aspects of the subject under test. To get the set of test cases structured in a better way, inner classes annotated with the @Nested annotation can be used which allows grouping tests together

 1@DisplayName("The AttachmentList")
 2class AttachmentListTest {
 3    private AttachmentList attachmentList;
 5    @BeforeEach
 6    void setUp() {
 7        attachmentList = new AttachmentList();
 8    }
10    @Nested
11    @DisplayName("when adding attachments")
12    class WhenAdding {
13        @Test
14        @DisplayName("should throw a NPE when trying to add null")
15        void nullObject() {
16            assertThrows(NullPointerException.class, () -> attachmentList.add(null));
17        }
18    }

and of course the inner classes can also be annotated with @DisplayName. Utilizing this we can form proper sentences which makes reading and understanding tests and its executions a real pleasure:

Test execution in IntelliJ with @DisplayName and @Nested annotated methods and classes

Adding these annotations causes only very little effort but helps all developers to understand much better what's going on in the code - especially after some time when the memory is not so fresh anymore or the developers who created the code initially have long left.

The full example project with all classes can be found on my GitLab instance.

Best Practices

This post is part of Best Practices, a series of short posts which summarize dos and don'ts I find useful in my daily work. Some of them are common sense, others might be a question of taste, some might even be controversial. All are my personal opinion. You see things differently? Leave me a comment on Mastodon or by E-Mail.