Mocking with Mockito

Replace collaborators such as payment gateways and mail senders with Mockito mocks, stub their answers, verify calls, and know when not to mock.

Syntax@ExtendWith(MockitoExtension.class) class ServiceTest { @Mock Gateway gateway; when(gateway.call("x")).thenReturn(result); verify(gateway).call("x"); }

A unit test for an OrderService should not charge a real card or send a real email. Mockito creates a stand-in object for an interface or class. You tell it what to return (stubbing) and afterwards ask what it was called with (verification). The service under test cannot tell the difference.

The core calls

  • @ExtendWith(MockitoExtension.class) with @Mock fields creates mocks for each test; @InjectMocks builds the class under test with them. Calling the constructor yourself in @BeforeEach is just as good and easier to follow.
  • when(mock.method(args)).thenReturn(value) or .thenThrow(exception). For void methods use doThrow(...).when(mock).method(args).
  • verify(mock).method(args), verify(mock, never()) and verify(mock, times(2)) check calls.
  • ArgumentCaptor grabs the object passed to a mock so you can assert on its fields.

An unstubbed method returns an empty value: null, 0, false, an empty collection or an empty Optional. That default often hides a missing stub.

When to mock

Mock outbound boundaries that are slow, cost money or have side effects: payment providers, email and SMS senders, message publishers, other teams' HTTP APIs. Put each behind a small interface you own, then mock that interface. Do not mock value objects, records or your own simple logic; use the real thing. Do not mock types you do not own, such as HttpClient, a JDBC Connection or a framework template. The mock encodes your assumption about their behaviour, which is exactly what may be wrong. Test SQL against a real database instead.

Mistakes people make

  • Verifying every call. Tests that check how the code works, not what it does, break on every refactor. Verify only calls that are the point of the behaviour, such as "the customer was emailed".
  • Mixing matchers and plain values. If one argument uses any() or eq(), all of them must.
  • Leftover stubs. MockitoExtension runs in strict mode and fails a test with UnnecessaryStubbingException when a stub is never used, which usually points at a wrong argument or dead setup.

Mockito 5 mocks final classes and records using an inline Java agent that attaches itself at run time. On JDK 21 and later the JVM prints a warning about dynamically loaded agents, and a future JDK is expected to refuse them by default. Mockito's documentation recommends adding it as a -javaagent in the test configuration.

Example

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {

    @Mock PaymentGateway payments;
    @Mock Mailer mailer;

    CheckoutService checkout;

    @BeforeEach
    void setUp() {
        checkout = new CheckoutService(payments, mailer);
    }

    @Test
    void sendsAReceiptWhenThePaymentSucceeds() {
        when(payments.charge("cus-1", new BigDecimal("19.99")))
                .thenReturn(new ChargeResult("ch-123", true));

        Receipt receipt = checkout.pay("cus-1", new BigDecimal("19.99"));

        assertThat(receipt.chargeId()).isEqualTo("ch-123");
        verify(mailer).sendReceipt("cus-1", "ch-123");
    }

    @Test
    void sendsNoReceiptWhenTheCardIsDeclined() {
        when(payments.charge("cus-1", new BigDecimal("19.99")))
                .thenReturn(new ChargeResult(null, false));

        checkout.pay("cus-1", new BigDecimal("19.99"));

        verify(mailer, never()).sendReceipt(any(), any());
    }
}

// The code under test

record ChargeResult(String chargeId, boolean succeeded) {}

record Receipt(String chargeId, boolean paid) {}

interface PaymentGateway {
    ChargeResult charge(String customerId, BigDecimal amount);
}

interface Mailer {
    void sendReceipt(String customerId, String chargeId);
}

class CheckoutService {
    private final PaymentGateway payments;
    private final Mailer mailer;

    CheckoutService(PaymentGateway payments, Mailer mailer) {
        this.payments = payments;
        this.mailer = mailer;
    }

    Receipt pay(String customerId, BigDecimal amount) {
        ChargeResult result = payments.charge(customerId, amount);
        if (result.succeeded()) {
            mailer.sendReceipt(customerId, result.chargeId());
        }
        return new Receipt(result.chargeId(), result.succeeded());
    }
}

When to use it

  • Test a checkout service's success and decline paths without charging a card at the payment provider.
  • Check that a signup flow publishes exactly one UserRegistered event with the right email.
  • Simulate a timeout from a partner API to prove the service answers 503 instead of crashing.

More examples

Capturing the object sent to a collaborator

The event is created inside the service, so the test cannot build an equal one in advance. Capturing it lets you assert on the fields that matter and ignore generated ones such as the id.

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

class SignupServiceTest {

    private final EventPublisher events = mock(EventPublisher.class);
    private final SignupService service = new SignupService(events);

    @Test
    void publishesAUserRegisteredEvent() {
        service.register("[email protected]");

        ArgumentCaptor<UserRegistered> captor = ArgumentCaptor.forClass(UserRegistered.class);
        verify(events).publish(captor.capture());

        UserRegistered event = captor.getValue();
        assertThat(event.email()).isEqualTo("[email protected]");
        assertThat(event.userId()).isNotBlank();
    }
}

// The code under test
record UserRegistered(String userId, String email) {}

interface EventPublisher {
    void publish(UserRegistered event);
}

class SignupService {
    private final EventPublisher events;

    SignupService(EventPublisher events) {
        this.events = events;
    }

    void register(String email) {
        // ... store the user, then announce it
        events.publish(new UserRegistered(UUID.randomUUID().toString(), email));
    }
}

Simulating a failing dependency

Timeouts and outages are hard to produce on demand with a real provider but trivial with a mock. The test proves the service wraps the low-level error in its own exception, which the API layer maps to a 503.

Example · java
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class CheckoutFailureTest {

    private final PaymentGateway payments = mock(PaymentGateway.class);
    private final Mailer mailer = mock(Mailer.class);
    private final CheckoutService checkout = new CheckoutService(payments, mailer);

    @Test
    void reportsAProviderOutageAsAServiceError() {
        // Every argument is a matcher: eq() for the id, any() for the amount
        when(payments.charge(eq("cus-1"), any(BigDecimal.class)))
                .thenThrow(new GatewayTimeoutException("provider did not answer in 5s"));

        assertThatThrownBy(() -> checkout.pay("cus-1", new BigDecimal("19.99")))
                .isInstanceOf(PaymentUnavailableException.class)
                .hasCauseInstanceOf(GatewayTimeoutException.class);
        verifyNoInteractions(mailer);
    }
}

// The code under test

record ChargeResult(String chargeId, boolean succeeded) {}

record Receipt(String chargeId, boolean paid) {}

interface PaymentGateway {
    ChargeResult charge(String customerId, BigDecimal amount);
}

interface Mailer {
    void sendReceipt(String customerId, String chargeId);
}

class GatewayTimeoutException extends RuntimeException {
    GatewayTimeoutException(String message) {
        super(message);
    }
}

class PaymentUnavailableException extends RuntimeException {
    PaymentUnavailableException(String message, Throwable cause) {
        super(message, cause);
    }
}

class CheckoutService {
    private final PaymentGateway payments;
    private final Mailer mailer;

    CheckoutService(PaymentGateway payments, Mailer mailer) {
        this.payments = payments;
        this.mailer = mailer;
    }

    Receipt pay(String customerId, BigDecimal amount) {
        ChargeResult result;
        try {
            result = payments.charge(customerId, amount);
        } catch (GatewayTimeoutException e) {
            throw new PaymentUnavailableException("payment provider unavailable", e);
        }
        if (result.succeeded()) {
            mailer.sendReceipt(customerId, result.chargeId());
        }
        return new Receipt(result.chargeId(), result.succeeded());
    }
}

Loading Mockito as a Java agent in Maven

The dependency plugin's properties goal exposes the mockito-core JAR path as a property, and Surefire passes it to the test JVM. Mockito is then loaded at startup, so the JDK 21+ warning about dynamically loaded agents goes away.

Example · xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <goals>
        <!-- sets a property per dependency holding the path to its JAR -->
        <goal>properties</goal>
      </goals>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <argLine>-javaagent:${org.mockito:mockito-core:jar}</argLine>
  </configuration>
</plugin>

Discussion

  • Be the first to comment on this lesson.