JUnit 5 Basics
JUnit 5 (also called Jupiter) is the test engine bundled with spring-boot-starter-test. Every test you write — unit, slice, or integration — is built on its annotations and lifecycle. This page covers the core JUnit 5 features you use daily, paired with AssertJ for fluent assertions.
Your first test
A test is a method annotated with @Test inside a class. JUnit discovers it automatically; the class needs no public modifier.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CalculatorTest {
@Test
void addsTwoNumbers() {
var calculator = new Calculator();
assertThat(calculator.add(2, 3)).isEqualTo(5);
}
}
Note: Import
org.junit.jupiter.api.Test— not the JUnit 4org.junit.Test. Mixing the two is the most common JUnit 5 mistake and silently skips tests.
Lifecycle callbacks
JUnit gives you hooks to set up and tear down state. By default a new instance of the test class is created per test method, so fields start fresh each time.
| Annotation | Runs | Static? |
|---|---|---|
@BeforeAll | Once, before all tests | Yes (unless @TestInstance(PER_CLASS)) |
@BeforeEach | Before every test | No |
@AfterEach | After every test | No |
@AfterAll | Once, after all tests | Yes |
class OrderServiceTest {
OrderService service;
@BeforeEach
void setUp() {
service = new OrderService(new InMemoryOrderRepository());
}
@AfterEach
void tearDown() {
// release resources opened per test
}
@Test
void startsEmpty() {
assertThat(service.findAll()).isEmpty();
}
}
Assertions with AssertJ
spring-boot-starter-test bundles AssertJ, whose assertThat(...) reads like a sentence and offers rich, type-aware matchers. Prefer it over JUnit’s plain Assertions.
assertThat(order.getTotal()).isEqualByComparingTo("99.90");
assertThat(items).hasSize(3).contains(milk).doesNotContain(eggs);
assertThat(user.getEmail()).isNotNull().endsWith("@example.com");
assertThat(response).extracting("status").isEqualTo(200);
Testing exceptions with assertThrows
To assert that code throws, use assertThrows (JUnit) or AssertJ’s assertThatThrownBy. Both capture the exception so you can assert on its message.
@Test
void rejectsNegativeAmount() {
// JUnit style — returns the thrown exception
var ex = assertThrows(IllegalArgumentException.class,
() -> account.withdraw(-10));
assertThat(ex.getMessage()).contains("must be positive");
// AssertJ style — fluent
assertThatThrownBy(() -> account.withdraw(-10))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must be positive");
}
Readable names with @DisplayName
@DisplayName replaces the method name in reports with a human sentence, which makes failures self-documenting.
@Test
@DisplayName("withdrawing more than the balance throws InsufficientFundsException")
void overdraftFails() {
assertThatThrownBy(() -> account.withdraw(1_000))
.isInstanceOf(InsufficientFundsException.class);
}
Data-driven tests with @ParameterizedTest
A @ParameterizedTest runs the same body once per input. Combine it with a source annotation; @ValueSource and @CsvSource cover most cases.
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t"})
@DisplayName("blank usernames are rejected")
void rejectsBlankUsernames(String input) {
assertThat(Validator.isValidUsername(input)).isFalse();
}
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"10, 5, 15",
"-1, 1, 0"
})
void adds(int a, int b, int expected) {
assertThat(new Calculator().add(a, b)).isEqualTo(expected);
}
Output:
adds(int, int, int) ✔
├─ [1] 2, 3, 5 ✔
├─ [2] 10, 5, 15 ✔
└─ [3] -1, 1, 0 ✔
Grouping with @Nested
@Nested inner classes group related tests and let you share a @BeforeEach for a sub-scenario, producing a tree in the report.
class AccountTest {
Account account = new Account(100);
@Nested
@DisplayName("when withdrawing")
class Withdrawing {
@Test
void reducesBalance() {
account.withdraw(40);
assertThat(account.getBalance()).isEqualTo(60);
}
@Test
void overdraftThrows() {
assertThatThrownBy(() -> account.withdraw(500))
.isInstanceOf(InsufficientFundsException.class);
}
}
}
Other useful annotations
@Disabled("reason")— temporarily skip a test (always give a reason).@RepeatedTest(5)— run a test multiple times to flush out flakiness.@Tag("slow")— categorize tests so the build can include/exclude them.@Timeout(2)— fail a test that runs longer than two seconds.
Tip: Keep one logical assertion per test and name the method after the behavior, not the method under test.
transferMovesMoneyBetweenAccounts()is far more useful in a failure report thantestTransfer().