Better Test Structure with JUnit 5
The Class Under Test
To demonstrate a well structured test, lets first define a relatively simple class to test: Title
Here are some functional properties of the Title
class:
- It is a value object.
- Wraps a
String
value. - Value can’t be
NULL
, empty, or otherwise ‘blank’. - Values are automatically trimmed.
- Equality is based on the wrapped value.
- Instances can only be created using the factory method.
A Value Object
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@EqualsAndHashCode @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Title { @Getter private final String value; public static Title of(String value) { if (StringUtils.isBlank(value)) { throw new IllegalArgumentException("[value] must not be blank!"); } return new Title(value.trim()); } @Override public String toString() { return value; } } |
The Test
Now lets create some tests for the Title
class. The following tests follow a simple set of rules:
- Each test needs a readable name stating the property it is testing. This is achieved by making use of the great new
@DisplayName
annotation. - If a property of the class under test can’t be tested with a single test, multiple tests are grouped together. This is done within an inner class annotated with
@Nested
. This will not only group them structurally within your test class, but also in the result view when executing them. - Grouped tests state what makes them special (what they are testing) without repeating the statement of the class they are located in.
A Value Object
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
@DisplayName("[Unit Test] Title Value Object") public class TitleTest { @Test @DisplayName("title's value is used as toString()") void valueUsedForToString() { Title title = Title.of("My Title"); assertThat(title.toString()).isEqualTo("My Title"); } @Test @DisplayName("titles can't be initialized directly") void titleCantBeInitializedDirectly() { for (Constructor constructor : Title.class.getConstructors()) { int modifiers = constructor.getModifiers(); assertThat(isPrivate(modifiers)).isTrue(); } } @Nested @DisplayName("titles can't be blank") public class TitleCantBeBlank { @Test @DisplayName("null") void nullValue() { titleMustNotBeBlank(null); } @Test @DisplayName("empty") void emptyValue() { titleMustNotBeBlank(""); } @Test @DisplayName("space") void spaceValue() { titleMustNotBeBlank(" "); } @Test @DisplayName("tab") void tabValue() { titleMustNotBeBlank("\t"); } @Test @DisplayName("line break") void lineBreakValue() { titleMustNotBeBlank("\n"); } void titleMustNotBeBlank(String value) { assertThat(expectThrows(IllegalArgumentException.class, () -> { Title.of(value); })).hasMessage("[value] must not be blank!"); } } @Nested @DisplayName("titles are trimmed") public class TitleAreTrimmed { @Test @DisplayName("spaces") void spaces() { Title title = Title.of(" trim me "); assertThat(title.getValue()).isEqualTo("trim me"); } @Test @DisplayName("tabs") void tabs() { Title title = Title.of("\ttrim me\t"); assertThat(title.getValue()).isEqualTo("trim me"); } @Test @DisplayName("line breaks") void lineBreaks() { Title title = Title.of("\ntrim me\n"); assertThat(title.getValue()).isEqualTo("trim me"); } } @Nested @DisplayName("equality is based on value") public class Equality { @Test @DisplayName("titles with same value are equal") void titlesWithSameValuesAreEqual() { Title foo1 = Title.of("foo"); Title foo2 = Title.of("foo"); assertThat(foo1).isEqualTo(foo2); } @Test @DisplayName("titles with different values are not equal") void titlesWithDifferentValuesAreNotEqual() { Title foo = Title.of("foo"); Title bar = Title.of("bar"); assertThat(foo).isNotEqualTo(bar); } } } |
The Result
When executed the above test will create the following result:
As you can see this approach can help you make sense, not only within your code, but also when looking at test results. Among other things, it makes it very easy to see if any relevant test is missing. You can check out the example’s code on GitHub.
In order to run the tests, you’ll need two things:
- IntelliJ IDE, since Eclipse does not support JUnit 5 (yet)
- The Project Lombok plugin in order to process the class under test’s annotations.
Recent posts

February 2023
Tioman Gally
Kotlin: How to Do Higher-Order Functions

September 2022
Filippos Vogiatzian & Javier De Haro Rodríguez
Maven beyond mvn clean install

December 2021
Alexander Miller & Sebastian Letzel
Kotlin Assertion Libraries - Atrium

November 2021
Tioman Gally
JUnit5 Custom Extension: ParameterResolver with Kotlin

July 2021
Sebastian Letzel & Alexander Miller
Kotlin Assertion Libraries - Strikt

July 2021
Eberhard Mayer
JIB – Build Docker images for your Java Application without a Docker Daemon
Comment article