Home/Learn/Java A–Z/JUnit 5 Testing

JUnit 5 Testing

Intermediate
Testing and Quality

JUnit 5 is the modern Java testing framework — annotations, parameterized tests, nested tests, and rich assertions for production-quality test suites.

Overview

JUnit 5 (Jupiter) is the current standard for Java unit testing. It introduces a clean annotation model (@Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll), powerful parameterized tests (@ParameterizedTest), nested test classes (@Nested), dynamic tests, and deep integration with build tools and IDEs. Combined with AssertJ for fluent assertions, it makes writing expressive, maintainable tests straightforward.

Core Annotations and Assertions

@Test marks a test method. @BeforeEach / @AfterEach run setup/teardown per test. @BeforeAll / @AfterAll run once per class (must be static). @Disabled skips a test. @DisplayName gives readable names in reports.

JUnit 5 assertions use org.junit.jupiter.api.Assertions. For fluent assertions, AssertJ (assertThat) is strongly preferred.

CalculatorTest.java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator();  // fresh instance per test
    }

    @AfterEach
    void tearDown() {
        // cleanup if needed
    }

    @Test
    @DisplayName("Adding two positive numbers returns their sum")
    void addPositiveNumbers() {
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void divideByZeroThrows() {
        assertThrows(ArithmeticException.class,
            () -> calc.divide(10, 0));
    }

    @Test
    void multipleAssertions() {
        assertAll("calculator",
            () -> assertEquals(4,  calc.add(2, 2)),
            () -> assertEquals(0,  calc.subtract(5, 5)),
            () -> assertEquals(6,  calc.multiply(2, 3))
        );  // all run even if one fails
    }

    @Test
    @Disabled("Calculator.sqrt not implemented yet")
    void sqrt() { /* ... */ }
}

Parameterized Tests

@ParameterizedTest runs the same test method with different arguments. Sources: @ValueSource, @CsvSource, @MethodSource, @EnumSource, @NullSource.

This eliminates repetitive test methods and makes edge-case coverage systematic.

ParameterizedTests.java
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;

class StringUtilsTest {

    // Simple values
    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t", "\n"})
    void isBlankReturnsTrueForBlankStrings(String input) {
        assertTrue(StringUtils.isBlank(input));
    }

    // Multiple columns from CSV
    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "0, 0, 0",
        "-1, 1, 0",
        "100, 200, 300"
    })
    void addReturnsCorrectSum(int a, int b, int expected) {
        assertEquals(expected, new Calculator().add(a, b));
    }

    // From a method (complex objects)
    @ParameterizedTest
    @MethodSource("provideUsers")
    void validateUserSucceeds(User user) {
        assertTrue(UserValidator.validate(user));
    }

    static Stream<User> provideUsers() {
        return Stream.of(
            new User("Alice", "alice@example.com", 30),
            new User("Bob",   "bob@example.com",   25)
        );
    }

    // All enum values
    @ParameterizedTest
    @EnumSource(DayOfWeek.class)
    void getDayNameIsNeverNull(DayOfWeek day) {
        assertNotNull(DateUtils.getDayName(day));
    }
}

Nested Tests and AssertJ

@Nested classes group related tests and share setup, making test structure mirror the code structure. AssertJ provides fluent, readable assertions with better error messages than plain JUnit assertions.

NestedTests.java
import org.assertj.core.api.Assertions.*;  // AssertJ

@DisplayName("OrderService")
class OrderServiceTest {

    @Nested
    @DisplayName("when placing an order")
    class PlaceOrder {
        private OrderService service;

        @BeforeEach
        void setUp() { service = new OrderService(mockRepo, mockPayment); }

        @Test
        @DisplayName("saves the order to the repository")
        void savesOrder() {
            Order order = service.place(new OrderRequest("item1", 2));
            // AssertJ — fluent and readable
            assertThat(order)
                .isNotNull()
                .extracting(Order::getStatus)
                .isEqualTo(OrderStatus.PENDING);
        }

        @Test
        void throwsWhenItemOutOfStock() {
            assertThatThrownBy(() -> service.place(new OrderRequest("rare", 100)))
                .isInstanceOf(OutOfStockException.class)
                .hasMessageContaining("rare");
        }
    }

    @Nested
    @DisplayName("when cancelling an order")
    class CancelOrder {
        @Test
        void returnsRefundAmount() {
            assertThat(service.cancel("order-123"))
                .isPositive()
                .isLessThanOrEqualTo(originalAmount);
        }
    }
}

Key Points to Remember

  • @BeforeEach / @AfterEach run per test method; @BeforeAll / @AfterAll run once per class.
  • @ParameterizedTest with @CsvSource or @MethodSource eliminates repetitive test methods.
  • assertAll() runs all assertions even if earlier ones fail — better for compound checks.
  • @Nested classes group related tests and can have their own @BeforeEach setup.
  • AssertJ provides fluent assertions with far better error messages than raw JUnit assertions.

Practice JUnit 5 Testing in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between @BeforeEach and @BeforeAll?

EasyAmazon
2

How do parameterized tests help with edge case coverage?

EasyGoogle
3

What does assertAll() do differently from multiple assertEquals() calls?

MediumOracle
4

How would you test that a method throws a specific exception?

EasyMicrosoft
5

What is the advantage of AssertJ over JUnit's built-in assertions?

MediumNetflix

Ask Aria about JUnit 5 Testing

Your personal AI tutor — ask anything about this concept