JUnit Nested Tests Example

In this article, we will discuss the usage of JUnit @Nested annotation with an example.

JUnit Jupiter @Nested annotation can be used to mark a nested class to be included in the test cases. When JUnit tests are executed, Nested classes are not scanned for test methods. We can explicitly mark them to be scanned for test cases using @Nested annotation.

JUnit 5 Nested Tests Example

To demo for the JUnit 5 nested tests feature, assume that we have an UserService class that has 4 methods: 
  • login()
  • logout()
  • changePassword()
  • resetPassword()
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 the above class.
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 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 = assertThrows(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"));
		}
	}

}

Note that we have grouped all test methods related to the Login feature in an inner class, and annotated the class with @Nested annotation:
  @Nested
  @DisplayName("Test Login Feature")
  class LoginFeature {
   // Reference above class

  }
Note that we have also grouped all test methods related to ChangePassword feature into an inner class and annotated the class with @Nested annotation:
  @Nested
  @DisplayName("Test ChangePassword Feature")
  class ChangePasswordFeature {
     //Reference above class
 
  }

Rules to add Nested Tests

When we add nested test classes to our test class, we have to follow these rules:
  1. All nested test classes must be non-static inner classes.
  2. We have to annotate our nested test classes with the @Nested annotation. This annotation ensures that JUnit 5 recognizes our nested test classes.
  3. There is no limit to the depth of the class hierarchy.
  4. By default, a nested test class can contain test methods, one @BeforeEach method, and one @AfterEach method.
  5. Because Java doesn't allow static members in inner classes, the @BeforeAll, and @AfterAll methods don't work by default.

Conclusion

Comments