JUnit 5 and Mockito Tutorial: How to Write Unit Tests
Write clear Java unit tests with JUnit Jupiter, then use Mockito to control collaborators when it helps. Includes Maven, Gradle, runnable examples, and troubleshooting.
Use JUnit Jupiter to call the unit under test and assert its result. Add Mockito when a collaborator needs a controlled response or when an important interaction is part of the behavior you want to specify. Keep simple deterministic objects real. This guide shows a complete Maven setup and runnable examples for both styles.
JUnit 5 is the umbrella for the JUnit Platform, Jupiter, and Vintage. Jupiter is the programming and extension model used for modern tests; the Platform runs test engines. Mockito supplies mock creation, stubbing, and verification. The examples below use Java 17, JUnit Jupiter 5.12.0, and Mockito 5.21.0; check your project’s Java baseline and selected library versions before copying the dependency versions.
1. Set up a Java project
For Maven, put application code in src/main/java and tests in src/test/java. Add the Jupiter aggregate dependency, Mockito core, and Mockito’s Jupiter integration. The extension artifact should match the Mockito version you selected.
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.12.0</junit.version>
<mockito.version>5.21.0</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
; <configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
</plugins>
</build>
Remove the stray semicolon before <configuration> if copying the snippet: the correct XML fragment is <configuration> directly inside the Surefire plugin. Run the suite with mvn test. JUnit’s guide explains the Platform, Jupiter, and Vintage architecture in its JUnit 5 User Guide.
For Gradle, use the matching test engine and Mockito integration. This Kotlin DSL example enables the JUnit Platform:
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.12.0")
testImplementation("org.mockito:mockito-core:5.21.0")
testImplementation("org.mockito:mockito-junit-jupiter:5.21.0")
}
tasks.test {
useJUnitPlatform()
}
Run it with ./gradlew test. If your project uses a different Java release, confirm that the selected Mockito release supports it and keep mockito-core and mockito-junit-jupiter aligned.
2. Write a plain JUnit Jupiter test
A test is a method marked with @Test. Call the code you are checking, then assert the expected observable result. This example is deliberately dependency-free:
package example;
public final class PriceCalculator {
public int totalCents(int unitPriceCents, int quantity) {
if (unitPriceCents < 0 || quantity < 0) {
throw new IllegalArgumentException("Values must not be negative");
}
return Math.multiplyExact(unitPriceCents, quantity);
}
}
package example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PriceCalculatorTest {
private final PriceCalculator calculator = new PriceCalculator();
@Test
void multipliesPriceByQuantity() {
int total = calculator.totalCents(250, 3);
assertEquals(750, total);
}
@Test
void rejectsNegativeQuantity() {
assertThrows(IllegalArgumentException.class,
() -> calculator.totalCents(250, -1));
}
}
The structure is arrange, act, assert: prepare inputs, invoke the unit, and check its result. Use @BeforeEach for setup repeated by each test when that makes the test easier to read; avoid hiding important test inputs in elaborate shared setup.
3. Decide when a dependency should be mocked
| Use a real object when… | Use a mock when… |
|---|---|
| The object is deterministic, fast, and simple, such as a value object or ordinary collection. | You need to control an external response, such as a payment gateway result. |
| The real behavior is part of what this test should cover. | The collaborator is costly, unpredictable, or outside this unit’s responsibility. |
| Mocking would only mirror implementation details. | A particular call or argument is itself part of the behavior being specified. |
Do not mock a dependency merely because it is injected. Mocking everything can make a test describe the current wiring instead of the behavior a caller relies on. Mockito’s API documentation describes mock creation, stubbing, verification, and matcher behavior.
4. Use Mockito with JUnit 5
Mockito’s Jupiter extension initializes fields annotated with @Mock and applies strict stubbing support. Register it with JUnit’s @ExtendWith. Here is a complete service example where a controlled gateway response affects the service result.
package example;
public record PaymentRequest(String orderId, int amountCents) {}
public record PaymentResult(boolean approved, String reference) {}
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
}
public final class PaymentService {
private final PaymentGateway gateway;
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
public String pay(PaymentRequest request) {
PaymentResult result = gateway.charge(request);
if (!result.approved()) {
return "DECLINED";
}
return "PAID:" + result.reference();
}
}
package example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock
PaymentGateway gateway;
@Test
void returnsReferenceWhenPaymentIsApproved() {
PaymentRequest request = new PaymentRequest("order-42", 1200);
when(gateway.charge(request))
.thenReturn(new PaymentResult(true, "pay-abc"));
PaymentService service = new PaymentService(gateway);
String outcome = service.pay(request);
assertEquals("PAID:pay-abc", outcome);
verify(gateway).charge(request);
}
@Test
void reportsDeclinedPayment() {
PaymentRequest request = new PaymentRequest("order-43", 1200);
when(gateway.charge(request))
.thenReturn(new PaymentResult(false, null));
PaymentService service = new PaymentService(gateway);
assertEquals("DECLINED", service.pay(request));
}
}
The result assertion is the primary contract check. The verification is useful here because sending this request to the gateway is part of the payment behavior. Add interaction verification selectively; routine verifyNoMoreInteractions() calls make tests brittle when harmless implementation details change. See the JUnit ExtendWith API and MockitoExtension API. Confirm compatibility and coordinates for your chosen Mockito release rather than assuming the surfaced extension API version is right for every project.
5. Cover multiple inputs with parameterized tests
Use a parameterized test when one rule should hold across several input cases. Add junit-jupiter-params if your build does not already provide it through its chosen JUnit aggregate, then use an argument source such as @CsvSource.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PriceCalculatorParameterizedTest {
private final PriceCalculator calculator = new PriceCalculator();
@ParameterizedTest
@CsvSource({
"100, 1, 100",
"250, 3, 750",
"0, 9, 0"
})
void calculatesTotals(int price, int quantity, int expected) {
assertEquals(expected, calculator.totalCents(price, quantity));
}
}
For larger datasets or named cases, consult the current Jupiter guide for sources such as method sources and CSV files. Keep each case understandable and give boundary conditions their own explicit tests when they represent distinct behavior.
6. Mockito details that prevent brittle tests
Stubbing return values and exceptions
For a normal mock, use when(call).thenReturn(value) or when(call).thenThrow(exception). An unstubbed method returns Mockito’s default for its return type; do not rely on an unstubbed mock to represent meaningful domain behavior. Stub the cases that drive the branch you are testing.
Argument matchers
Matchers such as any() and eq(value) are useful when the exact object is not relevant. If you use a matcher for one argument in a method invocation, use matchers for every argument in that invocation:
when(client.send(anyString(), eq("priority"))).thenReturn(true);
Do not mix a raw literal with a matcher in the same invocation. Prefer exact values when they make the expected behavior clearer.
Void methods and spies
For a void method, use Mockito’s doThrow(...).when(mock).method(...) style when you need to configure an exception. For spies, when(spy.method()).thenReturn(...) evaluates the real method while setting up the stub; use the doReturn, doThrow, or related family when invoking that real method would be unsafe or unwanted. A spy calls real methods by default, so use one only when that partial real behavior is intentional.
Lifecycle and manual construction
With MockitoExtension, let the extension initialize annotated mocks. Do not also call MockitoAnnotations.openMocks(this) for the same class. If avoiding annotations, construct mocks explicitly with mock(PaymentGateway.class); this can make dependencies visible, though the extension is convenient for larger fixture sets.
7. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| No tests found or tests are skipped | The build is not using the Jupiter engine or test task configuration. | For Gradle, configure useJUnitPlatform(). For Maven, use a current Surefire plugin and include Jupiter on the test classpath. |
@Test or Jupiter imports cannot be resolved |
The Jupiter dependency is absent, scoped incorrectly, or test sources are outside the build’s test source directory. | Check dependency scope, imports, and test file location. |
@Mock fields are null |
The test did not register MockitoExtension or initialize mocks another supported way. |
Add @ExtendWith(MockitoExtension.class) and ensure the matching Jupiter integration dependency is present. |
| Mockito reports unnecessary stubbing | A stub was set up but the tested path never used it; strict stubbing helps expose dead setup. | Remove the unused stub or correct the test path. Use leniency only for a deliberate shared setup case. |
| Invalid use of argument matchers | A matcher was combined with raw arguments, or a matcher was used outside a stubbing or verification call. | Use matchers consistently for all arguments in that invocation and only within Mockito stubbing or verification. |
| A spy unexpectedly performs real work | when(spy.method()) invoked the real method during stubbing, or the spy’s default behavior was overlooked. |
Prefer a mock when isolation is intended; otherwise use the appropriate doReturn/doThrow setup and account for real calls. |
| Dependency resolution or runtime compatibility errors | Core and Jupiter integration versions differ, or the selected library version does not match the project’s Java baseline. | Align Mockito artifacts, inspect the dependency tree, and select versions compatible with the configured JDK. |
8. Performance, reliability, and maintenance
- Keep unit tests local: mock network or other external boundaries so tests do not depend on service availability or credentials. Cover integration with separate tests where real wiring matters.
- Avoid time-based flakiness: pass clocks or time sources into logic that depends on time, so tests can use controlled values.
- Keep setup small: each test should make its meaningful inputs and expected outcome obvious. Shared fixtures should not conceal behavior.
- Test boundaries: include empty, minimum, maximum, invalid, and failure cases when those cases change behavior.
- Use mocks sparingly: excessive stubs and interaction checks increase maintenance cost and can make refactoring fail tests without changing behavior.
Unit tests normally avoid browser setup. If the behavior under test is a rendered web page, a screenshot can be a useful artifact for visual review or a downstream check, but it does not replace assertions about Java business logic.
9. Or skip the browser setup
For web pages involved in a broader test or review workflow, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF. See the ScreenshotNeo API documentation for the request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. AI agents can use its MCP server tools take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.
10. Frequently asked questions
Do I need JUnit Vintage for new tests?
Usually not. Vintage is for running tests written against older JUnit versions on the Platform; Jupiter is the model for new JUnit 5 tests.
Should I verify every call to a mock?
No. Assert returned values or state by default. Verify an interaction when the call itself is part of the behavior being promised.
Can I mock collections or simple value objects?
Prefer real collections and simple values. Mocking ordinary data structures adds setup without isolating meaningful behavior.
Why does MockitoExtension matter?
It connects Mockito’s annotated mocks and strict stubbing support to the Jupiter test lifecycle so fields such as @Mock are initialized for each test.


