A unit test should answer one question: given these inputs and these collaborators, does my class make the right decisions? Real collaborators get in the way. A payment gateway charges money, a repository needs a database and a clock never returns the same time twice. Mockito replaces those collaborators with objects whose behaviour you script and whose calls you can inspect afterwards, so the test controls everything except the code under test.

Mockito is easy to start with and easy to misuse. This article explains what happens inside a mock, the rules that cause most confusing errors, the build set-up modern JDKs need, and a worked test of a checkout service. The JUnit side is covered in the JUnit 5 article, and Spring context mocking in the Spring Boot article. Here the subject is Mockito itself.

Advertisement

Test doubles and where a mock fits

Gerard Meszaros named five kinds of test double, and the distinction still matters because Mockito can produce all of them:

DoubleWhat it doesMockito form
DummyFills a parameter, never usedmock(Foo.class) passed and ignored
StubReturns canned answerswhen(...).thenReturn(...)
SpyReal object, some calls recorded or overriddenspy(realObject)
MockRecords calls so the test can check themverify(mock).method(...)
FakeWorking lightweight implementationNot Mockito: write it yourself, such as an in-memory repository

The usual rule of thumb: stub queries, verify commands. If a collaborator returns data, stub it and assert on what your class does with the data. If a collaborator has a side effect that is the whole point, such as sending an email or saving a record, verify that the call happened with the right arguments. Verifying a query is usually a sign that the test describes the implementation rather than the behaviour.

How Mockito builds a mock

When you call mock(PaymentGateway.class), Mockito asks its configured mock maker for an instance whose methods do not run your code. Two strategies exist. The older one generated a subclass at runtime with ByteBuddy and overrode every method; it could not mock final classes, final methods or static methods, because a subclass cannot override them. The inline mock maker instead uses the Java instrumentation API to rewrite the bytecode of the real class so each method first checks whether the receiver is a mock. Since Mockito 5.0.0 the inline mock maker is the default, which is why final classes, records and Kotlin classes can be mocked without extra configuration.

Every mock owns a handler. Each call is recorded in an invocation log, then matched against the stubbings registered for that mock, latest first. If a stubbing matches, its answer runs; otherwise Mockito returns a default: null for objects, zero or false for primitives, empty collections, empty Optional and empty streams. Verification later searches the invocation log. Nothing in the process looks at your production code, which is why a mock cannot tell you whether the real collaborator would behave the same way.

What happens when a test calls a mocked methodTest codegateway.charge(order)Instrumented methodinline mock makerMockHandlerper-mock statecallinterceptInvocation logfor verify()Stubbingsmatchers -> answerrecordfind matchAnswerstub value or defaultreturn valueverify(gateway).charge(any())searches the invocation log, not the codeDefaults when nothing matchesnull, 0, false, empty collections, Optional.emptyMocks never run your real code unless you ask for it (spy, thenCallRealMethod).
A call on a mock is intercepted, logged for later verification, matched against stubbings and answered. Unmatched calls return type-appropriate defaults.
Advertisement

Running on JDK 21 and later

Inline mocking needs an instrumentation agent. Historically Mockito attached one to the running JVM by itself. JEP 451, delivered in JDK 21, makes the JVM print a warning when an agent is loaded dynamically and states that a future release will disallow it by default. The Mockito documentation therefore recommends loading Mockito as an agent at JVM start-up. With Maven the jar path comes from the dependency plugin's properties goal, so both plugins are needed. If no other plugin, such as JaCoCo, defines argLine, declare an empty <argLine/> property so the @{argLine} placeholder resolves:

<!-- pom.xml: load Mockito as a Java agent instead of self-attaching at runtime -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <goals><goal>properties</goal></goals>   <!-- defines ${org.mockito:mockito-core:jar} -->
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar}</argLine>
  </configuration>
</plugin>

With Gradle, put Mockito in a dedicated configuration and pass it to the test task:

// build.gradle.kts
val mockitoAgent = configurations.create("mockitoAgent")
dependencies {
    testImplementation("org.mockito:mockito-core:5.24.0")
    testImplementation("org.mockito:mockito-junit-jupiter:5.24.0")
    mockitoAgent("org.mockito:mockito-core:5.24.0") { isTransitive = false }
}
tasks.test {
    jvmArgs("-javaagent:${mockitoAgent.asPath}")
}

Mockito 5 requires Java 11 or later. If you still run tests on Java 8, you are on Mockito 4, and the advice in this article about default mock makers does not apply.

Stubbing semantics

when(mock.call(args)).thenReturn(value) works in an unusual way: Java evaluates mock.call(args) first, so the mock records an invocation, and when then turns that last invocation into a stubbing. Chained answers are consumed in order and the last one repeats: thenReturn(a, b) returns a, then b, then b forever. thenThrow raises an exception, and thenAnswer computes the result from the actual arguments, which is useful for echoing input or simulating ID generation.

Void methods cannot be used inside when(), so they use the do-family in reverse order: doThrow(new IOException()).when(mailer).send(any()). The do-family is also the correct choice for spies, because it never invokes the real method while stubbing.

Later stubbings override earlier ones for the same arguments. A shared default in a setup method and an override in one test therefore works, but it is also a common source of confusion when a broad any() stub silently shadows a specific one declared before it.

Matchers and captors

Argument matchers such as any(), eq(), anyString() and argThat(predicate) work by pushing a matcher onto a hidden thread-local stack and returning a dummy value. Mockito then pairs the stack with the parameters of the recorded call. That implementation produces the most common Mockito rule: if one argument uses a matcher, all arguments must. Mixing a raw value with a matcher raises InvalidUseOfMatchersException, and so does using a matcher outside a stubbing or verification.

Note that any() matches anything including null, while any(Foo.class) also checks the type and rejects null. When you need to inspect an argument in detail, prefer an ArgumentCaptor in the verification step rather than a complex argThat in the stubbing step. Captors keep stubbings simple and turn a vague does-not-match failure into a normal assertion on a concrete object.

Verification in detail

verify(mock).call(args) is shorthand for verify(mock, times(1)). Other modes state frequency explicitly: never(), atLeastOnce(), atMost(3) and times(n). Use them to express a business rule, such as charging exactly once, not to restate the loop structure of the implementation.

When order matters, as with releasing a lock after writing or publishing an event only after saving, create InOrder inOrder = inOrder(repo, events) and verify through it. Order verification is flexible: it checks that the listed calls happened in that relative order and ignores unlisted calls in between.

verifyNoMoreInteractions(mock) fails if any recorded call has not been verified. With strict stubs, calls that were used by a stubbing count as verified automatically, so the check focuses on unexpected commands. Use it sparingly, on collaborators where an extra call would be a real bug, such as a payment gateway, because applied everywhere it makes every harmless refactor a test failure. verifyNoInteractions is the stronger form, asserting that a mock was never touched at all.

Failure messages are the main reason to prefer verify over hand-rolled counters: Mockito prints the wanted invocation, the actual invocations with their arguments and the line where each happened.

Strict stubs

With MockitoExtension, strictness defaults to Strictness.STRICT_STUBS. Two behaviours follow. A stubbing that the test never uses fails the test with UnnecessaryStubbingException, which removes dead setup that misleads readers. And a call to a stubbed method with different arguments is reported as a potential stubbing problem instead of quietly returning null, which catches the classic bug where the code passes a slightly different object than the test expected.

When a stubbing is genuinely optional, such as a shared default used by only some tests, mark it with lenient().when(...) rather than relaxing the whole class with @MockitoSettings(strictness = Strictness.LENIENT). Lenient classes tend to collect stubs that no longer mean anything.

Worked example: a checkout service

The class under test charges an order through a gateway, records the outcome and handles a gateway timeout by marking the payment as unknown. An injected Clock makes time deterministic without any static mocking.

public final class CheckoutService {
    private final PaymentGateway gateway;
    private final OrderRepository orders;
    private final Clock clock;

    public CheckoutService(PaymentGateway gateway, OrderRepository orders, Clock clock) {
        this.gateway = gateway; this.orders = orders; this.clock = clock;
    }

    public Receipt checkout(Order order) {
        if (order.total().signum() <= 0) throw new IllegalArgumentException("empty order");
        ChargeResult r;
        try {
            r = gateway.charge(new ChargeRequest(order.id(), order.total(), order.idempotencyKey()));
        } catch (GatewayTimeoutException e) {
            orders.save(order.withStatus(Status.PAYMENT_UNKNOWN));
            throw new RetryLaterException(order.id(), e);
        }
        Order paid = order.withStatus(r.approved() ? Status.PAID : Status.DECLINED)
                          .withUpdatedAt(Instant.now(clock));
        orders.save(paid);
        return new Receipt(paid.id(), paid.status(), r.authCode());
    }
}

The tests cover approval, the timeout path and an input that must never reach the gateway:

@ExtendWith(MockitoExtension.class)              // strict stubs by default
class CheckoutServiceTest {
    @Mock PaymentGateway gateway;
    @Mock OrderRepository orders;
    @Captor ArgumentCaptor<Order> saved;
    Clock clock = Clock.fixed(Instant.parse("2026-10-02T10:00:00Z"), ZoneOffset.UTC);

    CheckoutService service;

    @BeforeEach void setUp() { service = new CheckoutService(gateway, orders, clock); }

    @Test void approvedChargeIsSavedAsPaid() {
        Order order = Orders.withTotal("42.00");
        when(gateway.charge(argThat(req -> req.amount().equals(new BigDecimal("42.00")))))
            .thenReturn(ChargeResult.approved("AUTH-1"));

        Receipt receipt = service.checkout(order);

        assertEquals(Status.PAID, receipt.status());
        verify(orders).save(saved.capture());
        assertEquals(Instant.parse("2026-10-02T10:00:00Z"), saved.getValue().updatedAt());
        verifyNoMoreInteractions(gateway);
    }

    @Test void timeoutMarksOrderUnknownAndAsksForRetry() {
        Order order = Orders.withTotal("10.00");
        when(gateway.charge(any())).thenThrow(new GatewayTimeoutException("read timeout"));

        assertThrows(RetryLaterException.class, () -> service.checkout(order));

        verify(orders).save(saved.capture());
        assertEquals(Status.PAYMENT_UNKNOWN, saved.getValue().status());
    }

    @Test void emptyOrderNeverReachesTheGateway() {
        assertThrows(IllegalArgumentException.class, () -> service.checkout(Orders.withTotal("0")));
        verifyNoInteractions(gateway, orders);
    }
}

Each test stubs only the query it needs, verifies the command that matters (orders.save), and uses a captor to assert on the saved order. The fixed clock removes the temptation to mock Instant.now() statically. The third test uses verifyNoInteractions to prove that validation happens before any side effect, a property that is hard to test any other way.

Spies, static mocks and construction mocks

A spy wraps a real object; unstubbed calls run real code. Spies are useful for legacy classes you cannot yet refactor, but a test full of spies usually means the class under test does too much. Static mocking with mockStatic and constructor interception with mockConstruction exist for the same reason. Both return a scoped object that must be closed, normally with try-with-resources, and both affect only the thread that created them, so code that calls the static method from an executor thread still sees the real implementation.

// 1. Mixing raw values and matchers -> InvalidUseOfMatchersException
when(repo.find("eu", anyInt())).thenReturn(x);        // wrong
when(repo.find(eq("eu"), anyInt())).thenReturn(x);    // right: all matchers or none

// 2. Stubbing a spy with when() runs the real method first
List<String> list = spy(new ArrayList<>());
when(list.get(0)).thenReturn("a");                     // IndexOutOfBoundsException
doReturn("a").when(list).get(0);                        // right: never calls get(0)

// 3. Static mocks must be closed, and only affect the creating thread
try (MockedStatic<UUID> uuid = mockStatic(UUID.class)) {
    uuid.when(UUID::randomUUID).thenReturn(FIXED);
    assertEquals(FIXED, ids.next());
}                                                       // real UUID.randomUUID() is back here

Failure modes and trade-offs

  • Over-mocking. Tests that mock every collaborator and verify every call break on each refactor while catching few bugs. Mock at architectural boundaries such as I/O, time, randomness and remote services, and use real objects for values and pure logic.
  • Mocking types you do not own. A mocked HTTP client or JDBC driver behaves the way you assumed the real one does. Wrap third-party APIs in your own small interface, mock that, and cover the wrapper with an integration test against the real dependency.
  • Mocks returning mocks. Deep stubs (RETURNS_DEEP_STUBS) hide violations of the Law of Demeter. If you need them, the design probably needs a narrower interface.
  • Leaking static mocks. An unclosed MockedStatic stays active for later tests on the same thread and causes order-dependent failures.
  • Async code. Verifying calls made on another thread races the test. Use verify(mock, timeout(1000)).call(), or better, inject an executor that runs tasks synchronously; the CompletableFuture article explains why.
  • Equals traps. Matching with eq() uses equals; records compare by value, ordinary classes by identity unless they override it.

The trade-off is speed and isolation against fidelity. Mockito tests run in milliseconds and pinpoint the failing class, but they cannot detect contract drift between your assumptions and the real collaborator. Pair them with a smaller number of integration tests at each boundary.

What to do next

  1. Upgrade to Mockito 5 and configure it as a Java agent in Maven or Gradle so tests keep working as JDKs tighten dynamic agent loading.
  2. Use MockitoExtension everywhere and leave strict stubs on; replace class-wide lenient settings with targeted lenient() calls.
  3. Search your suite for mockStatic and replace time and ID generation with injected Clock and supplier objects.
  4. Review one large test class: stub queries, verify commands, and delete verifications of pure reads.
  5. Wrap one third-party client in your own interface and add an integration test for the wrapper.
  6. Read the reflection article to understand the runtime machinery Mockito builds on.
Key takeaway: Mockito replaces collaborators with objects whose answers you script and whose calls you can verify, letting a unit test focus on one class. Since Mockito 5 the inline mock maker instruments real classes, so on modern JDKs load Mockito as a Java agent. Use matchers consistently, keep strict stubs on, capture arguments rather than writing clever matchers, and stub queries while verifying commands. Mock at real boundaries and back those mocks with integration tests.