Skip to content
Spring Boot sb testing 3 min read

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.Testnot the JUnit 4 org.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.

AnnotationRunsStatic?
@BeforeAllOnce, before all testsYes (unless @TestInstance(PER_CLASS))
@BeforeEachBefore every testNo
@AfterEachAfter every testNo
@AfterAllOnce, after all testsYes
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 than testTransfer().

Last updated June 13, 2026
Was this helpful?