Testing with JUnit 5 and Mockito

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

JUnit 5 (the "Jupiter" API) is the standard test framework for Java, and Mockito is the standard mocking library. Neither ships with the JDK and neither is covered by this section’s introductory books, so this page is grounded in the JUnit 5 User Guide and the Mockito Javadoc.

Setting Up JUnit 5

Add the aggregator junit-jupiter artifact on the test classpath. A current maven-surefire-plugin discovers and runs Jupiter tests with no extra configuration.

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.11.4</version>
  <scope>test</scope>
</dependency>

For Gradle, request the JUnit Platform on the test task:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') { useJUnitPlatform() }

Tests live under src/test/java in a package tree that mirrors src/main/java. A test class needs no base class or annotation of its own; each test method carries @Test.

package com.example;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @Test
    void addsTwoNumbers() {
        assertEquals(5, new Calculator().add(2, 3));
    }
}

Run them with mvn test or ./gradlew test — see Build and Tooling. Writing tests overall is covered in the User Guide under Writing Tests.

Assertions and the Lifecycle

Static methods on Assertions check outcomes: assertEquals, assertTrue / assertFalse, assertNull, assertThrows (returns the caught exception), assertAll (runs every check and reports all failures together), and assertTimeout. Lifecycle hooks — @BeforeEach / @AfterEach around every test, @BeforeAll / @AfterAll once per class — keep tests independent.

import org.junit.jupiter.api.*;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Account")
class AccountTest {

    Account account;

    @BeforeAll
    static void bootFixtures() { /* runs once, before any test in this class */ }

    @BeforeEach
    void freshAccount() { account = new Account(100); }   // new instance per test

    @Test
    @DisplayName("withdraw reduces the balance")
    void withdraw() {
        account.withdraw(30);
        assertEquals(70, account.balance());
    }

    @Test
    void overdraftIsRejected() {
        var ex = assertThrows(IllegalStateException.class, () -> account.withdraw(999));
        assertTrue(ex.getMessage().contains("insufficient"));
    }

    @Test
    void severalFactsAtOnce() {
        assertAll("account",
            () -> assertEquals(100, account.balance()),
            () -> assertFalse(account.isClosed()));
    }

    @Test
    void reconcilesQuickly() {
        assertTimeout(Duration.ofMillis(100), () -> account.reconcile());
    }

    @Test
    @Disabled("flaky until issue #123 is fixed")
    void legacyImport() { /* skipped, reported as disabled */ }

    @Test
    @Tag("slow")
    void fullMonthlyReport() {
        Assumptions.assumeTrue(System.getenv("CI") != null);   // abort (not fail) when false
        // ... expensive assertions ...
    }

    @Nested
    @DisplayName("when closed")
    class WhenClosed {
        @BeforeEach void close() { account.close(); }

        @Test
        void withdrawThrows() {
            assertThrows(IllegalStateException.class, () -> account.withdraw(1));
        }
    }

    @AfterEach
    void rollback() { /* runs after every test, even on failure */ }
}

@DisplayName sets the reported label, @Nested groups related tests under a shared setup, @Disabled skips one, @Tag marks it for selective runs (mvn test -Dgroups=slow), and Assumptions aborts a test whose preconditions are not met without counting it as a failure.

Parameterized Tests

Parameterized tests run one method once per input, each reported separately. Replace @Test with @ParameterizedTest and add a source: @ValueSource (literals), @CsvSource (comma-separated rows), @EnumSource (enum constants), or @MethodSource (a static method returning a Stream).

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.time.DayOfWeek;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;

class NumbersTest {

    @ParameterizedTest
    @ValueSource(ints = {2, 4, 100, -8})
    void areEven(int n) {
        assertEquals(0, n % 2);
    }

    @ParameterizedTest
    @CsvSource({ "1, 1, 2", "2, 3, 5", "10, -4, 6" })
    void adds(int a, int b, int expected) {
        assertEquals(expected, Calculator.add(a, b));
    }

    @ParameterizedTest
    @EnumSource(DayOfWeek.class)
    void everyDayHasALongName(DayOfWeek day) {
        assertTrue(day.name().length() >= 6);
    }

    @ParameterizedTest
    @MethodSource("blankStrings")
    void detectsBlank(String value) {
        assertTrue(value.isBlank());
    }

    static Stream<String> blankStrings() {
        return Stream.of("", "   ", "\t\n");
    }
}

By default JUnit creates a new test instance per method (Lifecycle.PER_METHOD), which is why @BeforeAll must be static. Annotate the class with @TestInstance(Lifecycle.PER_CLASS) to reuse one instance across the class and drop the static. For fluent, chainable checks many teams add AssertJ alongside JUnit — assertThat(account.balance()).isGreaterThan(50) — as a separate library.

Mocking with Mockito

A mock is a generated stand-in for a collaborator: you script what its methods return and later assert how they were called. @ExtendWith(MockitoExtension.class) initializes fields marked @Mock and injects them into an @InjectMocks target.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock PricingClient pricing;
    @Mock InventoryRepo inventory;
    @Mock AuditLog audit;
    @InjectMocks OrderService service;      // constructed with the three mocks injected

    @Test
    void totalsAnOrder() {
        when(pricing.unitPrice("sku-1")).thenReturn(250);
        when(inventory.inStock("sku-1")).thenReturn(true);

        int total = service.total("sku-1", 3);

        assertEquals(750, total);
        verify(pricing, times(1)).unitPrice("sku-1");     // called exactly once
        verify(inventory, never()).reserve(any());        // never called
    }

    @Test
    void propagatesAPricingOutage() {
        when(pricing.unitPrice(anyString())).thenThrow(new IllegalStateException("down"));
        assertThrows(IllegalStateException.class, () -> service.total("sku-9", 1));
    }

    @Test
    void recordsAnAuditEvent() {
        var captor = ArgumentCaptor.forClass(AuditEvent.class);
        service.total("sku-1", 1);
        verify(audit).record(captor.capture());
        assertEquals("sku-1", captor.getValue().sku());
    }
}

mock() and spy() also work without the extension. A spy wraps a real object: unstubbed methods run the real code.

import java.util.*;
import static org.mockito.Mockito.*;

List<String> fake = mock(List.class);
when(fake.size()).thenReturn(2);              // stubbed; everything else returns defaults

List<String> real = spy(new ArrayList<>());
real.add("x");                               // real behaviour runs
doReturn(99).when(real).size();              // ...but override selected methods

Use argument matchers (any(), eq(), anyString()) when you do not care about an exact value; if you use a matcher for one argument, use one for every argument of that call. The distinctions:

  • A stub just returns canned data so the code around it can run.

  • A mock additionally asserts on the interaction — that a method was called, how often, with what.

  • A spy keeps the real implementation and observes calls to it.

Do not mock types you do not own (JDBC, an HTTP client, a third-party SDK): wrap them behind your own interface and mock that, so a library upgrade cannot silently invalidate your stubs.

The JUnit 5 Test Lifecycle

For each test method the platform creates a fresh instance, runs @BeforeEach, runs the body, then runs @AfterEach — regardless of outcome — before moving to the next. @BeforeAll / @AfterAll bracket the whole loop once.

flowchart TD A["Launcher discovers @Test methods"] --> B[For each test method] B --> C[Create a new test instance] C --> D["Run @BeforeEach hooks"] D --> E["Run the @Test body"] E --> F["Run @AfterEach hooks"] F --> G{More test methods?} G -->|yes| B G -->|no| H[Aggregate results into the report]

See Also