In this tutorial, we will get to know about JUnit 5 Nested Tests, how to use the JUnit 5 @Nested annotation to express the relationship among several group of tests.
Let’s say we need to write tests for a class that has some functions and several of them have complex business domain logic. Normally, we can put on the tests for all methods of that class in the same test class. However, if we want to group all the test methods for those several complex methods together, or if we want to group all related test methods by features, we can use the JUnit 5 @Nested annotation to achieve those purposes.
Let’s try some examples about JUnit 5 Nested Tests below.
1. Preparation
The only one prerequisite for this tutorial is getting JUnit 5 be ready on your environment. You can refer to my recently post for getting JUnit 5 setup with Eclipse and build tools like Maven and Gradle and other JUnit 5 tutorials as well.
The sample source code for this post is on Github. You may want to take a look at it.
2. JUnit 5 Nested Tests
To demo for the JUnit 5 nested tests feature, assume that we have an UserService.java class which has 4 methods: login, logout, changePassword and resetPassword as below.
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 |
package xyz.howtoprogram.junit5.nested; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.platform.commons.util.StringUtils; public class UserService { public boolean login(String username, String password) { if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { throw new IllegalArgumentException("Username and password must not be null or empty"); } else if (username.equals("admin") && password.equals("password123")) { return true; } return false; } public boolean changePassword(long userId, String oldPassword, String newPassword) { if (userId == 1 && StringUtils.isNotBlank(newPassword) && StringUtils.isNotBlank(newPassword) && !newPassword.equals(oldPassword)) { return true; } return false; } public boolean resetPassword(long userId) { List<Long> existingUsers = new ArrayList<>(Arrays.asList(1L, 2L, 3L)); if (existingUsers.contains(userId)) { return true; } return false; } public boolean logout(long userId) { List<Long> existingUsers = new ArrayList<>(Arrays.asList(1L, 2L, 3L)); if (existingUsers.contains(userId)) { // do whatever } return true; } } |
We also assume that 2 methods: login and changePassword are complex and we want to group all the test methods of each into different groups by using the JUnit 5 @Nested annotation.
Let’s see the test class for above class.
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 |
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.expectThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @RunWith(JUnitPlatform.class) public class TestUserService { private UserService userService = null; @BeforeEach public void init() { userService = new UserService(); } @Test public void logoutSuccess() { long userId = 1L; assertTrue(userService.logout(userId)); } @Test public void resetPasswordExistingUser() { long userId = 1l; assertTrue(userService.resetPassword(userId)); } @Test public void resetPasswordUserNotExist() { long userId = 5l; assertFalse(userService.resetPassword(userId)); } @Nested @DisplayName("Test Login Feature") class LoginFeature { @Test void loginUsernamePasswordAreCorrect() { boolean actual = userService.login("admin", "password123"); assertTrue(actual); } @Test void loginUsernamePasswordAreInCorrect() { boolean actual = userService.login("admin", "password123456"); assertFalse(actual); } @Test void loginUsernamePasswordAreNulls() { IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { userService.login(null, null); }); assertEquals("Username and password must not be null or empty", exception.getMessage()); } @Test void loginUsernamePasswordAreEmpty() { assertThrows(IllegalArgumentException.class, () -> { userService.login("", ""); }); } } @Nested @DisplayName("Test ChangePassword Feature") class ChangePasswordFeature { @Test void changePasswordUserExistOldPasswordNewPasswordCorrect() { long userId = 1L; // existed user assertTrue(userService.changePassword(userId, "password123", "password123456")); } @Test void changePasswordUserNotExist() { long userId = 999L; // not existed user assertFalse(userService.changePassword(userId, "password123", "password123456")); } @Test void changePasswordUserExistOldPasswordAndNewPasswordEmpty() { long userId = 1L; // existed user assertFalse(userService.changePassword(userId, "", "")); } @Test void changePasswordUserExistOldPasswordEqualNewPassword() { long userId = 1L; // existed user assertFalse(userService.changePassword(userId, "password123", "password123")); } } } |
Below are some explanations.
2.1. Import new packages of JUnit Jupiter
We have to import classes from JUnit 5. All are stared with: org.junit.jupiter.api
2.2. To run the tests on Eclipse (Right click –> Run As –> JUnit Tests)
Temporarily, we need to add the JUnit 4 annotation: @RunWith(JUnitPlatform.class)
2.3. Group all test methods related to Login feature in an inner class, and annotate the class with @Nested
We create an inner class, annotate it with the @Nested annotation and put all tests methods related to the Login feature in this inner class.
1 2 3 4 5 6 |
@Nested @DisplayName("Test Login Feature") class LoginFeature { // Reference above class } |
2.4. Group all test methods related to ChangePassword feature into an inner class, and annotate the class with @Nested
1 2 3 4 5 6 |
@Nested @DisplayName("Test ChangePassword Feature") class ChangePasswordFeature { //Reference above class } |
2.5. All the tests for remaining methods, we still put in the outer class.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private UserService userService = null; @BeforeEach public void init() { userService = new UserService(); } @Test public void logoutSuccess() { long userId = 1L; assertTrue(userService.logout(userId)); } |
3. Run the test class and result.
To run the test on Eclipse, simply Right Click –> Run As –> JUnit Tests.
As for running with Maven and Gradle, you can refer to this post: JUnit 5 Basic Introduction
Here is the output on my Eclipse.

JUnit 5 Nested Tests
We can see that there are 2 groups for 2 features we defined above.
4. Conclusions.
We have learned about the JUnit 5 Nested Tests feature which allows developer to group all related tests in the same tests class together by using inner class and JUnit 5 @Nested annotation.
In future posts, we’ll continue to dive into other features of JUnit 5. Recently, I have some posts related to the JUnit 5. If you’re interested in, you can find them in the following links:
JUnit 5 Disable or Ignore A Test
JUnit 5 Test Suite – Aggregating Tests In Suites
JUnit 5 Assumptions With Assume
JUnit 5 Parameter Resolution Example