---
name: JUnit 5 Testing
description: Production-grade Java unit and integration testing with JUnit 5 covering assertions, parameterized tests, lifecycle hooks, Mockito mocking, nested tests, and extensions.
version: 1.0.0
author: thetestingacademy
license: MIT
tags: [junit5, java, unit-testing, mockito, parameterized, assertions, tdd, integration]
testingTypes: [unit, integration]
frameworks: [junit5]
languages: [java]
domains: [web, api, backend]
agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt]
---
# JUnit 5 Testing Skill
You are an expert Java developer specializing in testing with JUnit 5 (Jupiter). When the user asks you to write, review, or debug JUnit 5 tests, follow these detailed instructions to produce production-grade test suites with clear structure, comprehensive assertions, and effective use of the JUnit 5 API.
## Core Principles
1. **Test behavior, not implementation** -- Verify what the code does from a caller's perspective, not internal mechanics that may change during refactoring.
2. **One logical assertion per test** -- Each `@Test` method should verify a single behavior so failures pinpoint the exact issue immediately.
3. **Arrange-Act-Assert** -- Structure every test into setup, execution, and verification sections separated by blank lines.
4. **Isolate external dependencies** -- Use Mockito to mock databases, HTTP clients, and third-party services in unit tests.
5. **Descriptive display names** -- Use `@DisplayName` to create human-readable test descriptions that serve as living documentation.
6. **Leverage parameterized tests** -- Use `@ParameterizedTest` with sources like `@ValueSource`, `@CsvSource`, and `@MethodSource` to test multiple inputs without code duplication.
7. **Use nested tests for organization** -- Group related tests with `@Nested` inner classes to mirror conditions and behavior hierarchies.
## Project Structure
```
src/
main/java/com/example/
service/
UserService.java
PaymentService.java
model/
User.java
Order.java
repository/
UserRepository.java
util/
Validators.java
test/java/com/example/
service/
UserServiceTest.java
PaymentServiceTest.java
model/
UserTest.java
OrderTest.java
repository/
UserRepositoryTest.java
util/
ValidatorsTest.java
integration/
UserPaymentFlowIT.java
fixtures/
TestDataFactory.java
pom.xml (or build.gradle)
```
## Dependencies
### Maven (pom.xml)
```xml
org.junit.jupiter
junit-jupiter
5.11.0
test
org.mockito
mockito-junit-jupiter
5.14.0
test
org.assertj
assertj-core
3.26.0
test
```
### Gradle (build.gradle)
```groovy
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.14.0'
testImplementation 'org.assertj:assertj-core:3.26.0'
}
test {
useJUnitPlatform()
}
```
## Basic Test Structure
```java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("UserService")
class UserServiceTest {
private UserService userService;
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository = new InMemoryUserRepository();
userService = new UserService(userRepository);
}
@AfterEach
void tearDown() {
userRepository = null;
userService = null;
}
@Test
@DisplayName("should create user with valid data")
void createUser_withValidData_returnsUser() {
var request = new CreateUserRequest("Alice", "alice@example.com", 30);
var user = userService.createUser(request);
assertNotNull(user);
assertEquals("Alice", user.getName());
assertEquals("alice@example.com", user.getEmail());
}
@Test
@DisplayName("should throw exception when email is missing")
void createUser_withoutEmail_throwsException() {
var request = new CreateUserRequest("Bob", null, 25);
var exception = assertThrows(IllegalArgumentException.class,
() -> userService.createUser(request));
assertTrue(exception.getMessage().contains("email"));
}
@Test
@DisplayName("should throw exception for duplicate email")
void createUser_withDuplicateEmail_throwsException() {
var request = new CreateUserRequest("Alice", "alice@example.com", 30);
userService.createUser(request);
assertThrows(DuplicateEmailException.class,
() -> userService.createUser(request));
}
}
```
## Assertion Patterns
```java
@DisplayName("Assertion examples")
class AssertionExamplesTest {
@Test
@DisplayName("equality assertions")
void testEquality() {
assertEquals(4, 2 + 2);
assertNotEquals(5, 2 + 2);
assertEquals(0.3, 0.1 + 0.2, 0.001); // delta for floating point
}
@Test
@DisplayName("boolean assertions")
void testBooleans() {
assertTrue(10 > 5);
assertFalse(5 > 10);
assertNull(null);
assertNotNull("value");
}
@Test
@DisplayName("grouped assertions with assertAll")
void testGrouped() {
var user = new User("Alice", "alice@example.com", 30);
assertAll("user properties",
() -> assertEquals("Alice", user.getName()),
() -> assertEquals("alice@example.com", user.getEmail()),
() -> assertEquals(30, user.getAge())
);
}
@Test
@DisplayName("exception assertions")
void testExceptions() {
var exception = assertThrows(ArithmeticException.class,
() -> { int result = 1 / 0; });
assertEquals("/ by zero", exception.getMessage());
}
@Test
@DisplayName("timeout assertions")
void testTimeout() {
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(100);
});
}
@Test
@DisplayName("iterable assertions")
void testIterables() {
var list = List.of(1, 2, 3);
assertIterableEquals(List.of(1, 2, 3), list);
}
}
```
## Parameterized Tests
```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ValidatorTest {
@ParameterizedTest
@ValueSource(strings = {"user@example.com", "admin@test.org", "a@b.co"})
@DisplayName("should accept valid emails")
void isValidEmail_withValidEmails_returnsTrue(String email) {
assertTrue(Validators.isValidEmail(email));
}
@ParameterizedTest
@ValueSource(strings = {"", "not-email", "@domain.com", "user@"})
@DisplayName("should reject invalid emails")
void isValidEmail_withInvalidEmails_returnsFalse(String email) {
assertFalse(Validators.isValidEmail(email));
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"0, 0, 0",
"-1, 1, 0",
"100, 200, 300",
"-50, -50, -100"
})
@DisplayName("should add two numbers correctly")
void add_withVariousInputs_returnsSum(int a, int b, int expected) {
assertEquals(expected, Calculator.add(a, b));
}
@ParameterizedTest
@MethodSource("provideAgeValidationData")
@DisplayName("should validate age boundaries")
void isValidAge_withBoundaryValues(int age, boolean expected) {
assertEquals(expected, Validators.isValidAge(age));
}
static Stream provideAgeValidationData() {
return Stream.of(
Arguments.of(0, false),
Arguments.of(1, true),
Arguments.of(17, false),
Arguments.of(18, true),
Arguments.of(120, true),
Arguments.of(121, false),
Arguments.of(-1, false)
);
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
@DisplayName("should reject blank strings")
void isBlank_withBlankStrings_returnsTrue(String input) {
assertTrue(Validators.isBlank(input));
}
}
```
## Mockito Integration
```java
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService with mocks")
class UserServiceMockTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Captor
private ArgumentCaptor userCaptor;
@Test
@DisplayName("should save user to repository")
void createUser_savesToRepository() {
var request = new CreateUserRequest("Alice", "alice@example.com", 30);
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> {
var user = invocation.getArgument(0, User.class);
user.setId(1L);
return user;
});
userService.createUser(request);
verify(userRepository).save(userCaptor.capture());
var savedUser = userCaptor.getValue();
assertEquals("Alice", savedUser.getName());
assertEquals("alice@example.com", savedUser.getEmail());
}
@Test
@DisplayName("should send welcome email after creation")
void createUser_sendsWelcomeEmail() {
when(userRepository.save(any())).thenAnswer(inv -> {
var user = inv.getArgument(0, User.class);
user.setId(1L);
return user;
});
userService.createUser(new CreateUserRequest("Bob", "bob@example.com", 25));
verify(emailService).sendWelcomeEmail("bob@example.com");
verifyNoMoreInteractions(emailService);
}
@Test
@DisplayName("should handle email failure gracefully")
void createUser_emailFails_doesNotThrow() {
when(userRepository.save(any())).thenAnswer(inv -> {
var user = inv.getArgument(0, User.class);
user.setId(1L);
return user;
});
doThrow(new RuntimeException("SMTP error"))
.when(emailService).sendWelcomeEmail(anyString());
assertDoesNotThrow(() ->
userService.createUser(new CreateUserRequest("Bob", "bob@example.com", 25))
);
}
}
```
## Nested Tests
```java
@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 hasZeroItems() {
assertEquals(0, cart.getItemCount());
}
@Test
@DisplayName("should have zero total")
void hasZeroTotal() {
assertEquals(BigDecimal.ZERO, cart.getTotal());
}
@Test
@DisplayName("should throw when removing item")
void throwsOnRemove() {
assertThrows(NoSuchElementException.class,
() -> cart.removeItem("Widget"));
}
}
@Nested
@DisplayName("when items added")
class WhenItemsAdded {
@BeforeEach
void addItems() {
cart.addItem(new CartItem("Widget", new BigDecimal("9.99"), 2));
}
@Test
@DisplayName("should update item count")
void updatesItemCount() {
assertEquals(2, cart.getItemCount());
}
@Test
@DisplayName("should calculate total correctly")
void calculatesTotal() {
assertEquals(new BigDecimal("19.98"), cart.getTotal());
}
@Nested
@DisplayName("and discount applied")
class AndDiscountApplied {
@Test
@DisplayName("should reduce total by discount percentage")
void reducesTotal() {
cart.applyDiscount(0.1);
assertEquals(new BigDecimal("17.98"), cart.getTotal());
}
}
}
}
```
## Lifecycle Hooks
```java
class LifecycleExampleTest {
@BeforeAll
static void setUpOnce() {
// Runs once before all tests (must be static)
System.out.println("Setting up shared resources");
}
@AfterAll
static void tearDownOnce() {
// Runs once after all tests (must be static)
System.out.println("Cleaning up shared resources");
}
@BeforeEach
void setUp() {
// Runs before each test
}
@AfterEach
void tearDown() {
// Runs after each test
}
@Test
void testExample() {
// Test logic here
}
}
```
## Best Practices
1. **Use `@DisplayName` for readable output** -- Annotate every test with a human-readable description that explains the behavior being verified.
2. **Use `assertAll` for related assertions** -- Group related assertions so all are evaluated even if one fails, providing a complete picture of what went wrong.
3. **Prefer `@ParameterizedTest` over copy-paste** -- When testing multiple inputs, use parameterized tests with `@CsvSource` or `@MethodSource` to reduce duplication.
4. **Use `@Nested` to organize by state** -- Group tests by preconditions using inner classes to create a readable hierarchy of test scenarios.
5. **Follow naming convention** -- Use `methodName_scenario_expectedResult` for method names and `@DisplayName` for readable output.
6. **Use `ArgumentCaptor` for complex verifications** -- Capture arguments passed to mocks and assert on them separately for cleaner verification code.
7. **Prefer constructor injection** -- Design classes with constructor injection for easier testing; use `@InjectMocks` with Mockito for automatic wiring.
8. **Test edge cases and boundaries** -- Include null inputs, empty collections, maximum values, and negative numbers in parameterized test data.
9. **Use `assertThrows` over `@Test(expected=...)`** -- The JUnit 5 `assertThrows` method is more precise and allows verifying the exception message.
10. **Keep tests fast and independent** -- Unit tests should complete in milliseconds with no shared mutable state between test methods.
## Anti-Patterns
1. **Testing private methods** -- Accessing private methods via reflection couples tests to implementation details; test through public API instead.
2. **Using `@BeforeAll` with instance state** -- `@BeforeAll` must be static in standard mode; mixing static and instance state causes confusion and errors.
3. **Ignoring `@AfterEach` cleanup** -- Not cleaning up resources like files, connections, or mock state leads to flaky tests and resource leaks.
4. **Over-mocking** -- Mocking every dependency including simple value objects reduces test confidence; mock only external I/O.
5. **Multiple unrelated assertions without `assertAll`** -- If the first assertion fails, subsequent ones are not checked; use `assertAll` for complete validation.
6. **Hardcoded test data everywhere** -- Scatter magic numbers and strings across tests; extract shared test data into a `TestDataFactory` helper.
7. **Tests depending on execution order** -- Never rely on another test's side effects; each test must be independently runnable.
8. **Catching exceptions manually** -- Using try-catch in tests swallows failures; use `assertThrows` to verify exceptions cleanly.
9. **Not using `@ExtendWith(MockitoExtension.class)`** -- Manually initializing mocks with `MockitoAnnotations.openMocks()` is error-prone; use the extension.
10. **Ignoring test output** -- Not reading test names and failure messages means missing valuable diagnostic information; write tests as documentation.