Labs ICT
โญ Pro Login

@Nested Tests for Organization

When your test class grows beyond a handful of tests, it becomes hard to navigate. @Nested lets you group related tests into inner classes, creating a logical hierarchy. It is like organizing your tests into chapters and sections.

Basic @Nested Structure


class StackTest {

    private Stack<String> stack;

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

    @Nested
    @DisplayName("when stack is empty")
    class EmptyStack {

        @Test
        @DisplayName("should have size zero")
        void sizeIsZero() {
            assertEquals(0, stack.size());
        }

        @Test
        @DisplayName("should return true for isEmpty()")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("should throw EmptyStackException on pop")
        void popThrowsException() {
            assertThrows(EmptyStackException.class,
                () -> stack.pop());
        }
    }

    @Nested
    @DisplayName("when stack has elements")
    class StackWithElements {

        @BeforeEach
        void pushElements() {
            stack.push("first");
            stack.push("second");
            stack.push("third");
        }

        @Test
        @DisplayName("should have correct size")
        void correctSize() {
            assertEquals(3, stack.size());
        }

        @Test
        @DisplayName("should return last pushed element on pop")
        void popReturnsLastElement() {
            assertEquals("third", stack.pop());
        }

        @Test
        @DisplayName("should peek at top element without removing")
        void peekReturnsTopElement() {
            assertEquals("third", stack.peek());
            assertEquals(3, stack.size()); // Size unchanged
        }
    }
}
    

How @Nested Works

Each @Nested class is a separate test class with its own lifecycle. The key thing to understand:


class MyTest {

    @BeforeEach
    void outerSetUp() {
        System.out.println("Outer @BeforeEach");
    }

    @Nested
    class InnerGroup {

        @BeforeEach
        void innerSetUp() {
            System.out.println("Inner @BeforeEach");
        }

        @Test
        void myTest() {
            System.out.println("Test");
        }
    }
}

// Output:
// Outer @BeforeEach
// Inner @BeforeEach
// Test
    

The outer @BeforeEach runs first, then the inner one. This means inner classes inherit the setup from outer classes.

Deep Nesting

You can nest as deep as you want (though 2-3 levels is usually enough):


class OrderTest {

    @Nested
    class CreatingOrder {

        @Test
        void orderIsCreated() { }

        @Nested
        class WithValidItems {
            @Test
            void orderContainsItems() { }
        }

        @Nested
        class WithInvalidItems {
            @Test
            void orderIsRejected() { }
        }
    }
}
    

Nested Test Reporting

JUnit produces a nested report structure. Here is what it looks like with @DisplayName:


OrderTest
โ”œโ”€โ”€ CreatingOrder
โ”‚   โ”œโ”€โ”€ order is created
โ”‚   โ”œโ”€โ”€ WithValidItems
โ”‚   โ”‚   โ””โ”€โ”€ order contains items
โ”‚   โ””โ”€โ”€ WithInvalidItems
โ”‚       โ””โ”€โ”€ order is rejected
    

This is far more readable than a flat list of 50 test methods.

Practical Example: Testing a Bank Account


class BankAccountTest {

    private BankAccount account;

    @BeforeEach
    void setUp() {
        account = new BankAccount(1000);
    }

    @Nested
    @DisplayName("deposit")
    class Deposit {

        @Test
        @DisplayName("should increase balance")
        void increasesBalance() {
            account.deposit(500);
            assertEquals(1500, account.getBalance());
        }

        @Test
        @DisplayName("should reject negative amount")
        void rejectsNegativeAmount() {
            assertThrows(IllegalArgumentException.class,
                () -> account.deposit(-100));
        }
    }

    @Nested
    @DisplayName("withdraw")
    class Withdraw {

        @Test
        @DisplayName("should decrease balance when sufficient funds")
        void decreasesBalance() {
            account.withdraw(300);
            assertEquals(700, account.getBalance());
        }

        @Test
        @DisplayName("should throw when insufficient funds")
        void throwsOnInsufficientFunds() {
            assertThrows(InsufficientFundsException.class,
                () -> account.withdraw(2000));
        }

        @Nested
        @DisplayName("when balance is zero")
        class ZeroBalance {
            @BeforeEach
            void drainAccount() {
                account.withdraw(1000);
            }

            @Test
            @DisplayName("should always throw")
            void alwaysThrows() {
                assertThrows(InsufficientFundsException.class,
                    () -> account.withdraw(1));
            }
        }
    }

    @Nested
    @DisplayName("transfer")
    class Transfer {

        @Test
        @DisplayName("should move money between accounts")
        void movesMoneyBetweenAccounts() {
            BankAccount other = new BankAccount(500);
            account.transfer(other, 200);
            assertEquals(800, account.getBalance());
            assertEquals(700, other.getBalance());
        }
    }
}
    

Best Practices

  • Use @Nested to group tests by feature or scenario
  • Give each nested class a descriptive @DisplayName
  • Use @BeforeEach in nested classes for group-specific setup
  • Keep nesting to 2-3 levels max
  • Avoid tests that depend on the execution order within a nested class

๐Ÿงช Quick Quiz

What does @Nested allow you to do?