JUnit 5 Nested Tests Example

1. Overview

Nested tests give the test writer more capabilities to express the relationship among several groups of tests.
Example: 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.


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.
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"));
    }
  }

}

From the above example, let's discuss a few points like
1. Import new packages of JUnit Jupiter 
We have to import classes from JUnit 5. All are stared with: org.junit.jupiter.api
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)
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.
  @Nested
  @DisplayName("Test Login Feature")
  class LoginFeature {
   // Reference above class

  }
4. Group all test methods related to ChangePassword feature into an inner class and annotate the class with @Nested
  @Nested
  @DisplayName("Test ChangePassword Feature")
  class ChangePasswordFeature {
     //Reference above class
 
  }
5. All the tests for the remaining methods, we still put in the outer class. For example:
  private UserService userService = null;
 
  @BeforeEach
  public void init() {
    userService = new UserService();
  }
 
  @Test
  public void logoutSuccess() {
    long userId = 1L;
    assertTrue(userService.logout(userId));
 
  }

Nested test suite for testing a stack

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

3. Conclusion

Comments