Nested Tests in JUnit: Structuring Complex Test Scenarios

Illustration for Nested Tests in JUnit: Structuring Complex Test Scenarios
By Last updated:

As applications evolve, testing scenarios become more complex. A flat list of test methods may work for small projects, but it quickly turns into clutter when handling advanced workflows, domain-driven logic, or multi-layered microservices. To solve this, JUnit 5 introduces nested tests via the @Nested annotation, allowing developers to group related tests and create hierarchical structures that mirror real-world business logic.

In this tutorial, we’ll dive into nested tests in JUnit 5, explain why they matter, walk through practical examples, and discuss best practices for structuring complex test suites in enterprise Java projects.


Why Nested Tests Matter

  • Clarity: Related test cases are grouped together logically.
  • Readability: Tests follow a hierarchy that mirrors the structure of the code.
  • Maintainability: Easier to extend and modify specific scenarios.
  • Scalability: Keeps large test classes manageable.
  • CI/CD Readiness: Suites remain organized for continuous integration pipelines.

Think of nested tests like chapters in a book: instead of having one giant page of text, you split your story into well-organized sections.


Basic Example of Nested Tests

import org.junit.jupiter.api.*;

class UserServiceTest {

    private UserService userService;

    @BeforeEach
    void setUp() {
        userService = new UserService();
    }

    @Nested
    class CreateUserTests {
        @Test
        void shouldCreateUserSuccessfully() {
            User user = userService.createUser("Alice");
            Assertions.assertNotNull(user);
        }

        @Test
        void shouldFailWhenNameIsEmpty() {
            Assertions.assertThrows(IllegalArgumentException.class,
                () -> userService.createUser(""));
        }
    }

    @Nested
    class DeleteUserTests {
        @Test
        void shouldDeleteExistingUser() {
            userService.createUser("Bob");
            userService.deleteUser("Bob");
            Assertions.assertTrue(userService.isDeleted("Bob"));
        }

        @Test
        void shouldThrowWhenUserNotFound() {
            Assertions.assertThrows(UserNotFoundException.class,
                () -> userService.deleteUser("Unknown"));
        }
    }
}

Here, CreateUserTests and DeleteUserTests group tests logically, improving readability.


Lifecycle in Nested Tests

  • @BeforeAll and @AfterAll must be static by default.
  • Each @Nested class has its own @BeforeEach and @AfterEach lifecycle.
  • Parent and nested lifecycles are executed in order.
@Nested
class NestedExample {

    @BeforeEach
    void beforeEach() {
        System.out.println("Before each nested test");
    }

    @Test
    void sampleTest() {
        System.out.println("Running nested test");
    }
}

Combining Nested Tests with Parameterized Tests

Nested tests also work with parameterized tests, providing rich data-driven scenarios.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class MathUtilsTest {

    @Nested
    class FactorialTests {

        @ParameterizedTest
        @ValueSource(ints = {0, 1, 5})
        void shouldCalculateFactorial(int input) {
            int result = factorial(input);
            Assertions.assertTrue(result >= 1);
        }

        int factorial(int n) {
            return (n <= 1) ? 1 : n * factorial(n - 1);
        }
    }
}

Real-World Use Cases

  1. Spring Boot Apps
    Group tests by REST controllers, service layers, and repositories.

  2. Microservices
    Use nested tests to separate contract testing, integration with Kafka, and DB verification with Testcontainers.

  3. Legacy Codebases
    Gradually organize chaotic tests by introducing nested structures for domain-specific logic.


Nested Tests with Mockito and Testcontainers

Mockito Example

import org.junit.jupiter.api.*;
import org.mockito.Mockito;

class OrderServiceTest {

    private OrderRepository repository;
    private OrderService service;

    @BeforeEach
    void setUp() {
        repository = Mockito.mock(OrderRepository.class);
        service = new OrderService(repository);
    }

    @Nested
    class PlaceOrderTests {

        @Test
        void shouldSaveOrder() {
            Order order = new Order("Book");
            service.placeOrder(order);
            Mockito.verify(repository).save(order);
        }
    }
}

Testcontainers Example

import org.junit.jupiter.api.*;
import org.testcontainers.containers.PostgreSQLContainer;

class DatabaseIntegrationTest {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @BeforeAll
    static void setupAll() {
        postgres.start();
    }

    @Nested
    class UserTableTests {
        @Test
        void shouldInsertUser() {
            System.out.println("JDBC URL: " + postgres.getJdbcUrl());
            Assertions.assertTrue(postgres.isRunning());
        }
    }

    @AfterAll
    static void teardownAll() {
        postgres.stop();
    }
}

Best Practices

  • Use @Nested only when grouping adds value.
  • Avoid overly deep nesting — 2–3 levels max.
  • Use descriptive class names (CreateUserTests, not InnerClass1).
  • Combine with tags (@Tag("integration")) for CI/CD filtering.
  • Treat nested classes as units of behavior, not just folders.

Version Tracker

  • JUnit 4 → JUnit 5: Nested tests were introduced in JUnit 5 (not available in JUnit 4).
  • Mockito Updates: Static and final mocking support aligns well with nested unit testing.
  • Testcontainers Growth: Expanded modules for databases, Kafka, LocalStack — perfect for nested integration tests.

Conclusion & Key Takeaways

Nested tests in JUnit 5 bring structure and clarity to complex test scenarios. They allow developers to group related tests, maintain readable suites, and scale testing for modern enterprise applications.

Key Takeaways:

  • Use nested tests for logical grouping.
  • Combine with parameterized tests, Mockito, and Testcontainers.
  • Avoid deep hierarchies; focus on clarity.
  • Nested tests improve maintainability and CI/CD readiness.

FAQ

1. What is the main benefit of nested tests in JUnit 5?
They group related test cases together for clarity and maintainability.

2. Can nested tests have their own lifecycle methods?
Yes, @BeforeEach and @AfterEach work independently in each nested class.

3. How deep can nested tests go?
Technically unlimited, but best practice is no more than 2–3 levels.

4. Do nested tests work with parameterized tests?
Yes, nested classes fully support @ParameterizedTest.

5. Can I use assumptions inside nested tests?
Yes, you can use assumeTrue or assumingThat as in normal tests.

6. Do nested tests work with Mockito?
Absolutely, mocks can be initialized in each nested class.

7. Can I run only a specific nested test class?
Yes, IDEs and build tools like Maven/Gradle support running nested tests.

8. How do nested tests appear in test reports?
They show as hierarchical structures, improving readability.

9. Can I use Testcontainers with nested tests?
Yes, containers can be started in parent or nested lifecycles.

10. Should I migrate JUnit 4 tests to nested tests?
Yes, if grouping improves clarity and maintainability in large test classes.