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.
See Also
-
Build and Tooling — wiring
junit-jupiterinto Maven or Gradle and running the suite from the build. -
Exceptions —
assertThrowsand testing the failure paths. -
High-Level Concurrency —
assertTimeoutand testing code that runs asynchronously. -
Annotations and Reflection — how
@Testand@ExtendWithare discovered and processed.