How do I organize tests with nested test classes?

Organizing Tests with Nested Test Classes

Nested test classes are a great way to group related tests inside a single test class. In Java, this is commonly done with JUnit 5 using @Nested.

They help you structure tests around:

  • A specific method
  • A scenario
  • A state of the object under test
  • Success vs failure cases
  • Different user roles or permissions
  • Different validation paths

Basic Example

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

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    private final Calculator calculator = new Calculator();

    @Nested
    class AddTests {

        @Test
        void shouldAddTwoPositiveNumbers() {
            int result = calculator.add(2, 3);

            assertEquals(5, result);
        }

        @Test
        void shouldAddNegativeNumbers() {
            int result = calculator.add(-2, -3);

            assertEquals(-5, result);
        }
    }

    @Nested
    class DivideTests {

        @Test
        void shouldDivideTwoNumbers() {
            int result = calculator.divide(10, 2);

            assertEquals(5, result);
        }

        @Test
        void shouldThrowWhenDividingByZero() {
            // test exception case here
        }
    }
}

A Common Organization Style

A useful pattern is:

class UserServiceTest {

    @Nested
    class CreateUser {

        @Test
        void shouldCreateUserWhenInputIsValid() {
        }

        @Test
        void shouldRejectDuplicateEmail() {
        }

        @Test
        void shouldRejectInvalidEmail() {
        }
    }

    @Nested
    class UpdateUser {

        @Test
        void shouldUpdateUserWhenUserExists() {
        }

        @Test
        void shouldThrowWhenUserDoesNotExist() {
        }
    }

    @Nested
    class DeleteUser {

        @Test
        void shouldDeleteUserWhenUserExists() {
        }

        @Test
        void shouldDoNothingWhenUserDoesNotExist() {
        }
    }
}

This makes the test report easier to read:

UserServiceTest
 ├─ CreateUser
 │   ├─ shouldCreateUserWhenInputIsValid
 │   ├─ shouldRejectDuplicateEmail
 │   └─ shouldRejectInvalidEmail
 ├─ UpdateUser
 │   ├─ shouldUpdateUserWhenUserExists
 │   └─ shouldThrowWhenUserDoesNotExist
 └─ DeleteUser
     ├─ shouldDeleteUserWhenUserExists
     └─ shouldDoNothingWhenUserDoesNotExist

Using @BeforeEach in Nested Classes

Each nested class can have its own setup.

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

class OrderServiceTest {

    private OrderService orderService;

    @BeforeEach
    void setUp() {
        orderService = new OrderService();
    }

    @Nested
    class WhenOrderIsNew {

        private Order order;

        @BeforeEach
        void setUp() {
            order = new Order("NEW");
        }

        @Test
        void shouldAllowCancellation() {
            // test new order cancellation
        }

        @Test
        void shouldAllowPayment() {
            // test new order payment
        }
    }

    @Nested
    class WhenOrderIsShipped {

        private Order order;

        @BeforeEach
        void setUp() {
            order = new Order("SHIPPED");
        }

        @Test
        void shouldNotAllowCancellation() {
            // test shipped order cancellation
        }
    }
}

The outer @BeforeEach runs before the nested class @BeforeEach.

Execution order is:

OrderServiceTest.setUp()
WhenOrderIsNew.setUp()
test method

Recommended Naming Styles

Option 1: Method-based grouping

class ProductServiceTest {

    @Nested
    class FindById {

        @Test
        void shouldReturnProductWhenFound() {
        }

        @Test
        void shouldThrowWhenProductDoesNotExist() {
        }
    }

    @Nested
    class Save {

        @Test
        void shouldSaveValidProduct() {
        }

        @Test
        void shouldRejectProductWithoutName() {
        }
    }
}

Option 2: Scenario-based grouping

class CheckoutServiceTest {

    @Nested
    class WhenCartIsEmpty {

        @Test
        void shouldRejectCheckout() {
        }
    }

    @Nested
    class WhenCartHasItems {

        @Test
        void shouldCreateOrder() {
        }

        @Test
        void shouldClearCartAfterCheckout() {
        }
    }
}

Both are valid. For service classes, I usually prefer method-based grouping. For complex domain behavior, scenario-based grouping often reads better.


Best Practices

Do

  • Use @Nested to group related test cases.
  • Keep nesting shallow, usually one level deep.
  • Use descriptive nested class names.
  • Use @BeforeEach inside nested classes for scenario-specific setup.
  • Keep test method names behavior-focused.

Example:

@Test
void shouldThrowExceptionWhenEmailAlreadyExists() {
}

Avoid

  • Deep nesting like 3–4 levels unless absolutely necessary.
  • Putting unrelated tests in the same nested class.
  • Sharing too much mutable state between nested classes.
  • Making nested class names vague, such as Tests1 or Validations.

With Spring Boot / Spring MVC Tests

Nested tests also work with Spring test annotations.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Nested
    class CreateUser {

        @Test
        void shouldReturnCreatedWhenRequestIsValid() throws Exception {
            // mockMvc.perform(...)
        }

        @Test
        void shouldReturnBadRequestWhenEmailIsInvalid() throws Exception {
            // mockMvc.perform(...)
        }
    }

    @Nested
    class GetUser {

        @Test
        void shouldReturnUserWhenFound() throws Exception {
            // mockMvc.perform(...)
        }

        @Test
        void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
            // mockMvc.perform(...)
        }
    }
}

Rule of Thumb

Use nested test classes when your flat test class starts looking like this:

@Test
void createUserShouldSucceedWhenInputIsValid() {
}

@Test
void createUserShouldFailWhenEmailExists() {
}

@Test
void updateUserShouldSucceedWhenUserExists() {
}

@Test
void updateUserShouldFailWhenUserDoesNotExist() {
}

Refactor it into:

class UserServiceTest {

    @Nested
    class CreateUser {
    }

    @Nested
    class UpdateUser {
    }
}

That usually makes the tests easier to scan, maintain, and understand.