Nested and Repeated Tests in JUnit 5
As your test classes grow, flat lists of test methods become hard to navigate. @Nested provides structure, and @RepeatedTest helps find flaky tests.
@Nested — Grouping Related Tests
@DisplayName("UserService")
class UserServiceTest {
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService(new InMemoryUserRepository());
}
@Nested
@DisplayName("createUser")
class CreateUser {
@Test
@DisplayName("should create user with valid input")
void shouldCreateUser() {
User user = userService.createUser("Alice", "alice@example.com");
assertNotNull(user.getId());
assertEquals("Alice", user.getName());
}
@Test
@DisplayName("should throw when name is blank")
void shouldThrowWhenNameBlank() {
assertThrows(IllegalArgumentException.class,
() -> userService.createUser("", "alice@example.com"));
}
@Test
@DisplayName("should throw when email is duplicate")
void shouldThrowWhenEmailDuplicate() {
userService.createUser("Alice", "alice@example.com");
assertThrows(DuplicateEmailException.class,
() -> userService.createUser("Bob", "alice@example.com"));
}
}
@Nested
@DisplayName("findUser")
class FindUser {
@Test
@DisplayName("should find existing user by ID")
void shouldFindById() {
User created = userService.createUser("Alice", "alice@example.com");
User found = userService.findById(created.getId());
assertEquals("Alice", found.getName());
}
@Test
@DisplayName("should return null for non-existent ID")
void shouldReturnNullForMissing() {
assertNull(userService.findById(999L));
}
}
@Nested
@DisplayName("deleteUser")
class DeleteUser {
private User existingUser;
@BeforeEach
void createTestUser() {
existingUser = userService.createUser("Alice", "alice@example.com");
}
@Test
@DisplayName("should delete existing user")
void shouldDeleteUser() {
userService.deleteById(existingUser.getId());
assertNull(userService.findById(existingUser.getId()));
}
@Test
@DisplayName("should throw when deleting non-existent user")
void shouldThrowForMissing() {
assertThrows(UserNotFoundException.class,
() -> userService.deleteById(999L));
}
}
}
IDE Output
UserService
├── createUser
│ ├── ✓ should create user with valid input
│ ├── ✓ should throw when name is blank
│ └── ✓ should throw when email is duplicate
├── findUser
│ ├── ✓ should find existing user by ID
│ └── ✓ should return null for non-existent ID
└── deleteUser
├── ✓ should delete existing user
└── ✓ should throw when deleting non-existent user
Nested Lifecycle Rules
- Outer
@BeforeEachruns before inner@BeforeEach. - Inner classes inherit outer lifecycle methods.
- Inner classes cannot have
@BeforeAll/@AfterAll(unless usingPER_CLASS).
class OuterTest {
@BeforeEach
void outerSetup() {
System.out.println("1. Outer @BeforeEach");
}
@Nested
class InnerTest {
@BeforeEach
void innerSetup() {
System.out.println("2. Inner @BeforeEach");
}
@Test
void test() {
System.out.println("3. Test runs");
}
}
}
// Output: 1 → 2 → 3
@RepeatedTest — Run Multiple Times
Useful for detecting flaky tests — tests that pass sometimes and fail other times.
@RepeatedTest(5)
void shouldAlwaysReturnPositive() {
int result = randomService.generatePositiveNumber();
assertTrue(result > 0);
}
With Custom Display Name
@RepeatedTest(value = 3, name = "Attempt {currentRepetition} of {totalRepetitions}")
void shouldBeConsistent() {
assertEquals(42, calculator.compute());
}
Output:
Attempt 1 of 3 ✓
Attempt 2 of 3 ✓
Attempt 3 of 3 ✓
RepetitionInfo — Access Repetition Details
@RepeatedTest(5)
void testWithRepetitionInfo(RepetitionInfo info) {
int current = info.getCurrentRepetition(); // 1, 2, 3, 4, 5
int total = info.getTotalRepetitions(); // 5
System.out.println("Running repetition " + current + " of " + total);
// Use different data per repetition
}
When to Use What
| Tool | Use Case |
|---|---|
@Nested |
Organize tests by method/scenario |
@RepeatedTest |
Detect flaky tests, test randomness |
@ParameterizedTest |
Same logic, different inputs |
@Tag |
Filter by category (fast, slow) |
Real-World Pattern: BDD-Style Nesting
@DisplayName("ShoppingCart")
class ShoppingCartTest {
private ShoppingCart cart;
@BeforeEach
void setUp() {
cart = new ShoppingCart();
}
@Nested
@DisplayName("when empty")
class WhenEmpty {
@Test
@DisplayName("should have zero items")
void shouldHaveZeroItems() {
assertEquals(0, cart.getItemCount());
}
@Test
@DisplayName("should have zero total")
void shouldHaveZeroTotal() {
assertEquals(BigDecimal.ZERO, cart.getTotal());
}
}
@Nested
@DisplayName("after adding one item")
class AfterAddingOne {
@BeforeEach
void addItem() {
cart.add(new Product("Laptop", new BigDecimal("999.99")));
}
@Test
@DisplayName("should have one item")
void shouldHaveOneItem() {
assertEquals(1, cart.getItemCount());
}
@Test
@DisplayName("should show correct total")
void shouldShowTotal() {
assertEquals(new BigDecimal("999.99"), cart.getTotal());
}
@Nested
@DisplayName("then removing it")
class ThenRemoving {
@BeforeEach
void removeItem() {
cart.removeAll();
}
@Test
@DisplayName("should be empty again")
void shouldBeEmpty() {
assertEquals(0, cart.getItemCount());
}
}
}
}
Key Takeaways
@Nestedturns flat test classes into organized hierarchies.- Inner classes inherit outer
@BeforeEach/@AfterEach. @RepeatedTestruns the same test N times — great for flaky test detection.- BDD-style nesting (
when...,then...) improves readability. - Don’t nest more than 2-3 levels deep — it gets hard to follow.
What’s Next?
Next: Mockito Basics — mock dependencies so you can test classes in isolation.