Mocking Beans in Spring Boot Tests with @MockitoBean

Introduction

This tutorial explains how to replace a Spring bean with a mock in a test, using Spring Framework's @MockitoBean and @MockitoSpyBean. We will see what a mock is and when to use one, and test an order service whose payment client must never call the real payment provider. First with plain Mockito and no Spring at all, then inside a Spring context, with stubbing, exceptions, verification, an argument captor and a spy. We will also see that @MockBean from older tutorials no longer exists in Spring Boot 4, and how Mockito's strict stubs report a stub that the code never uses.

What a mock is

A mock is a stand-in object that implements the same type as the real one, but does only what the test tells it. Asked a question it was not told about, it returns a default: null, 0, false or an empty collection. It also records every call, so that the test can check afterwards how it was used. Mocks are the right tool for dependencies that are slow, unreliable, expensive or have side effects: a payment provider, an email server, another team's service. They are the wrong tool for simple, fast code: a real object, as the stock check here, tests more and costs nothing.

Mockito, part of Spring Boot's test starter, creates mocks, and offers two styles for stubbing them. The BDD style used here reads as given-when-then: given(payments.charge("Ann", 900)).willReturn("PAY-42"). The classic style, when(...).thenReturn(...), does the same.

The application

An order service checks the stock, charges the customer through a payment client, and sends a confirmation. The payment client stands for a call to a real payment provider; here it simply throws, so that a test that accidentally uses it fails at once -
src/main/java/.../Order.java
//Order.java
package com.javabrahman.testing.mocks;

public record Order(String customer, String isbn, int quantity, int amount) {
}
src/main/java/.../PaymentClient.java - calls an external payment provider
//PaymentClient.java
package com.javabrahman.testing.mocks;

import org.springframework.stereotype.Component;

@Component
public class PaymentClient {

  // In a real application this sends an HTTP request to a payment provider - not something a test should do
  public String charge(String customer, int amount) {
    throw new IllegalStateException("no network access in this example: the real provider is not reachable");
  }
}
src/main/java/.../Stock.java
//Stock.java
package com.javabrahman.testing.mocks;

import java.util.Map;
import org.springframework.stereotype.Component;

@Component
public class Stock {
  private final Map<String, Integer> copies = Map.of("978-0134685991", 5, "978-0132350884", 0);

  public boolean available(String isbn, int quantity) {
    return copies.getOrDefault(isbn, 0) >= quantity;
  }
}
src/main/java/.../Notifier.java
//Notifier.java
package com.javabrahman.testing.mocks;

import org.springframework.stereotype.Component;

@Component
public class Notifier {

  public String confirmation(Order order, String paymentId) {
    return "Dear " + order.customer() + ", order paid (" + paymentId + ")";
  }

  public void send(String customer, String message) {
    System.out.println("   [email to " + customer + "] " + message);
  }
}
src/main/java/.../OrderService.java - the class under test
//OrderService.java
package com.javabrahman.testing.mocks;

import org.springframework.stereotype.Service;

@Service
public class OrderService {
  private final Stock stock;
  private final PaymentClient payments;
  private final Notifier notifier;

  public OrderService(Stock stock, PaymentClient payments, Notifier notifier) {
    this.stock = stock;
    this.payments = payments;
    this.notifier = notifier;
  }

  public String place(Order order) {
    if (!stock.available(order.isbn(), order.quantity())) {
      return "REJECTED: out of stock";
    }
    String paymentId;
    try {
      paymentId = payments.charge(order.customer(), order.amount());
    } catch (RuntimeException e) {
      return "FAILED: " + e.getMessage();
    }
    notifier.send(order.customer(), notifier.confirmation(order, paymentId));
    return "PLACED: " + paymentId;
  }
}
src/main/java/.../ShopApp.java
//ShopApp.java
package com.javabrahman.testing.mocks;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShopApp {

  public static void main(String[] args) {
    SpringApplication.run(ShopApp.class, args);
  }
}

Mocks without Spring

The order service is an ordinary class with three constructor parameters, so a unit test needs no Spring at all. MockitoExtension creates a mock for each @Mock field, and @InjectMocks calls the service's constructor with them -
src/test/java/.../OrderServiceUnitTest.java - plain Mockito, no Spring
//OrderServiceUnitTest.java
package com.javabrahman.testing.mocks;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class OrderServiceUnitTest {

  @Mock Stock stock;
  @Mock PaymentClient payments;
  @Mock Notifier notifier;
  @InjectMocks OrderService service;          // created with the three mocks above

  @Test
  void placesAnOrder() {
    given(stock.available("978-0134685991", 2)).willReturn(true);
    given(payments.charge("Ann", 900)).willReturn("PAY-1");
    given(notifier.confirmation(any(), anyString())).willReturn("thanks");

    assertThat(service.place(new Order("Ann", "978-0134685991", 2, 900))).isEqualTo("PLACED: PAY-1");
    verify(notifier).send("Ann", "thanks");
  }

  @Test
  void neverChargesWhenOutOfStock() {
    given(stock.available(anyString(), anyInt())).willReturn(false);

    assertThat(service.place(new Order("Raj", "978-0132350884", 1, 380))).startsWith("REJECTED");
    verifyNoInteractions(payments, notifier);
  }
}

Mocks inside a Spring context: @MockitoBean

When a test needs Spring - an integration test, or a slice such as the @WebMvcTest of the previous tutorialClick to read tutorial: Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and More - the mock must be in the application context, in place of the real bean, so that the beans that depend on it receive the mock -
  • @MockitoBean on a test field replaces the bean of that type with a Mockito mock, or adds one if there is none, and injects it into the field.
  • @MockitoSpyBean wraps the existing bean in a spy: the real methods run, and every call is recorded for verify(). Individual methods can still be stubbed.
Both belong to Spring Framework's test support since version 6.2, in org.springframework.test.context.bean.override.mockito. They replace Spring Boot's @MockBean and @SpyBean, which were deprecated in Spring Boot 3.4 and are gone in Spring Boot 4: code using them no longer compiles, and the fix is a change of annotation and import. @MockitoBean and @MockitoSpyBean in a test context - the real OrderService gets the real Stock, a Mockito mock in place of PaymentClient, and the real Notifier wrapped in a spy
src/test/java/.../OrderServiceSpringTest.java - @MockitoBean and @MockitoSpyBean
//OrderServiceSpringTest.java
package com.javabrahman.testing.mocks;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;

@SpringBootTest
class OrderServiceSpringTest {

  @Autowired
  OrderService service;                       // the real service, from the context

  @MockitoBean
  PaymentClient payments;                     // replaces the PaymentClient bean with a mock

  @MockitoSpyBean
  Notifier notifier;                          // wraps the real Notifier bean: real behaviour, but recorded

  @Test
  void chargesAndNotifies() {
    given(payments.charge("Ann", 900)).willReturn("PAY-42");

    assertThat(service.place(new Order("Ann", "978-0134685991", 2, 900))).isEqualTo("PLACED: PAY-42");

    ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
    verify(notifier).send(anyString(), message.capture());
    System.out.println("   captured message: " + message.getValue());
    assertThat(message.getValue()).contains("PAY-42");
  }

  @Test
  void reportsAFailedPayment() {
    willThrow(new IllegalStateException("card declined")).given(payments).charge(anyString(), anyInt());

    assertThat(service.place(new Order("Raj", "978-0134685991", 1, 450))).isEqualTo("FAILED: card declined");
    verify(notifier, never()).send(anyString(), anyString());
  }

  @Test
  void eachTestGetsAFreshMock() {
    // no stubbing here: an unstubbed mock returns null, and the stubbing of the other tests is gone
    assertThat(payments.charge("Ann", 900)).isNull();
    verify(payments, times(1)).charge("Ann", 900);
  }

  @Test
  void theRealStockIsUsed() {
    assertThat(service.place(new Order("Eve", "978-0132350884", 1, 380))).startsWith("REJECTED");
    verify(payments, never()).charge(anyString(), anyInt());
  }
}
 OUTPUT - plain Mockito and @MockitoBean
   [email to Ann] Dear Ann, order paid (PAY-42)
   captured message: Dear Ann, order paid (PAY-42)
╷
└─ JUnit Jupiter ✔
   ├─ OrderServiceUnitTest ✔
   │  ├─ placesAnOrder() ✔
   │  └─ neverChargesWhenOutOfStock() ✔
   └─ OrderServiceSpringTest ✔
      ├─ reportsAFailedPayment() ✔
      ├─ chargesAndNotifies() ✔
      ├─ eachTestGetsAFreshMock() ✔
      └─ theRealStockIsUsed() ✔

[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         6 tests found           ]
[         0 tests skipped         ]
[         6 tests started         ]
[         0 tests aborted         ]
[         6 tests successful      ]
[         0 tests failed          ]

Explanation of the output

  • Plain Mockito: placesAnOrder() stubbed all three dependencies, and verify(notifier).send("Ann", "thanks") checked that the confirmation went out. neverChargesWhenOutOfStock() stubbed the stock check with the matchers anyString() and anyInt(), and verifyNoInteractions(payments, notifier) proved that a rejected order charges nobody.
  • A spy really runs: the line "[email to Ann] Dear Ann, order paid (PAY-42)" was printed by the real Notifier, because @MockitoSpyBean keeps the real behaviour. The ArgumentCaptor caught the message passed to send(), so the test could print it and check that it contains the payment id.
  • An exception from a mock: willThrow(...).given(payments).charge(...) made the payment fail, the service answered "FAILED: card declined", and verify(notifier, never()).send(...) confirmed that no email was sent. For methods returning void, this form - or the classic doThrow().when() - is needed, since given(...) cannot take a void call.
  • Mocks are reset: in eachTestGetsAFreshMock(), the mock had no stubbing - the "PAY-42" of another test was gone - so it returned null, and times(1) counted only this test's call. Spring resets @MockitoBean mocks after every test.
  • Real and mock beans together: theRealStockIsUsed() ran against the real Stock bean, which has no copies of "Clean Code", and the mock proved that the payment provider was never called.

Strict stubs: a stub nobody uses

MockitoExtension uses strict stubs by default: a stubbing that the test never uses makes the test fail. That sounds harsh, but an unused stub usually means the test does not test what its author thought, or is left over from older code -
src/test/java/.../UnusedStubbingTest.java - a stub the code never uses
//UnusedStubbingTest.java
package com.javabrahman.testing.mocks;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class UnusedStubbingTest {

  @Mock Stock stock;
  @Mock PaymentClient payments;
  @Mock Notifier notifier;
  @InjectMocks OrderService service;

  @Test
  void outOfStock() {
    given(stock.available("978-0132350884", 1)).willReturn(false);
    given(payments.charge("Raj", 380)).willReturn("PAY-7");     // never used: the order is rejected first

    assertThat(service.place(new Order("Raj", "978-0132350884", 1, 380))).startsWith("REJECTED");
  }
}
 OUTPUT - Mockito's strict stubs (the lines about the failure)
   └─ UnusedStubbingTest ✔
      └─ outOfStock() ✘
               Unnecessary stubbings detected.
                 1. -> at com.javabrahman.testing.mocks.UnusedStubbingTest.outOfStock(UnusedStubbingTest.java:24)
  JUnit Jupiter:UnusedStubbingTest:outOfStock()
Unnecessary stubbings detected.
  1. -> at com.javabrahman.testing.mocks.UnusedStubbingTest.outOfStock(UnusedStubbingTest.java:24)
[         0 tests successful      ]
[         1 tests failed          ]
The assertion passed - the order was rejected - and yet the test failed with "Unnecessary stubbings detected", pointing at line 24, the stubbing of payments.charge. The order never reached the payment, so that stub was dead code. The fix is to delete it, which also leaves the test describing exactly what the code does. Where a stub is used only by some of the tests of a shared setup, lenient().when(...) marks it as allowed to stay unused.

Good practice with mocks

  • Mock what you do not own and cannot run: external services, clocks, random numbers. Do not mock simple value objects or your own fast helpers.
  • Prefer plain Mockito for unit tests of a single class: it needs no context and runs in milliseconds. Use @MockitoBean when the test needs Spring around the mock.
  • Verify what matters: a payment that must not happen, an email that must be sent. Verifying every call couples the test to the implementation, and it breaks with each refactoring.
  • Mind the context cache: every distinct combination of @MockitoBean fields is a different context. Many test classes with slightly different mocks each start their own context, a common reason for slow test suites, covered in testing best practicesClick to read tutorial: Spring Boot Testing Best Practices and Faster Test Suites.
  • Mockito as a Java agent: on recent JDKs, Mockito should be loaded as a Java agent instead of attaching itself at runtime, or the JDK prints a warning, and will refuse it in a future release. The Maven Surefire and Gradle setups in Spring Boot projects can pass -javaagent with the Mockito jar to the test JVM; the tests in this series run that way.

Summary

In this tutorial we replaced dependencies with mocks. With plain Mockito, @Mock and @InjectMocks tested the order service without Spring, stubbing with given().willReturn() and checking with verify() and verifyNoInteractions(). Inside a Spring context, @MockitoBean replaced the payment client bean, and @MockitoSpyBean wrapped the real notifier, whose email was really printed and whose message an ArgumentCaptor caught. A thrown exception, never(), and the reset of mocks between tests completed the picture, and Mockito's strict stubs failed a test because of a stub the code never used. @MockBean and @SpyBean are gone in Spring Boot 4; these annotations replace them. The next tutorial tests REST APIs in depth: MockMvc and MockMvcTesterClick to read tutorial: Testing REST APIs with MockMvc and MockMvcTester.