Testing Spring Boot Applications

Mocking Beans with @MockBean

18 min Lesson 7 of 13

Mocking Beans with @MockBean

Every Spring Boot application is a graph of collaborating beans. When you write a slice test — a @WebMvcTest for a controller, for example — Spring wires only a portion of that graph. The beans your slice does not load (services, repositories, external clients) still appear as dependencies. @MockBean solves exactly this problem: it registers a Mockito mock as a Spring bean inside the test application context, satisfying the dependency without loading the real implementation.

How @MockBean Differs from @Mock

Mockito's @Mock (or Mockito.mock()) creates a mock object that lives entirely outside the Spring context. You wire it into your subject manually, usually through a constructor or a setter. That works fine for pure unit tests where you construct the class yourself.

@MockBean, by contrast, tells Spring's test support to replace (or create) a bean of the given type in the ApplicationContext. Any other bean that has that type injected will receive the mock automatically — no manual wiring needed. Mockito still creates the underlying mock, so all the familiar when(…).thenReturn(…) and verify(…) APIs work exactly as you would expect.

Key rule: use @Mock for plain unit tests (no Spring context). Use @MockBean whenever you need Spring to wire collaborators for you — inside @WebMvcTest, @DataJpaTest, or @SpringBootTest slices.

A Realistic Example: Controller Slice with a Mocked Service

Suppose you have an OrderController that delegates to an OrderService. Loading the real OrderService would pull in a JPA repository, a database connection, and possibly an email client. None of that belongs in a controller test. Here is the full pattern:

// OrderService.java package com.example.orders; import java.util.List; public interface OrderService { List<OrderDto> findAllForUser(Long userId); OrderDto getById(Long id); }
// OrderController.java package com.example.orders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/orders") public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @GetMapping("/user/{userId}") public ResponseEntity<List<OrderDto>> listForUser(@PathVariable Long userId) { return ResponseEntity.ok(orderService.findAllForUser(userId)); } @GetMapping("/{id}") public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) { return ResponseEntity.ok(orderService.getById(id)); } }
// OrderControllerTest.java package com.example.orders; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.List; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(OrderController.class) class OrderControllerTest { @Autowired private MockMvc mockMvc; @MockBean // replaces OrderService in the context private OrderService orderService; @Test void listForUser_returnsOrdersAsJson() throws Exception { // Arrange OrderDto dto = new OrderDto(1L, "PENDING", 49.99); given(orderService.findAllForUser(42L)).willReturn(List.of(dto)); // Act & Assert mockMvc.perform(get("/api/orders/user/42").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").value(1)) .andExpect(jsonPath("$[0].status").value("PENDING")); verify(orderService).findAllForUser(42L); } }

Notice three things: the annotation is on the field, not on a parameter; the mock is reset automatically before each test method; and given(…).willReturn(…) (BDD-style) and when(…).thenReturn(…) (classic style) are interchangeable — pick one and stay consistent.

@MockBean Inside @SpringBootTest

You can also use @MockBean in a full @SpringBootTest when you want to load the entire context but isolate one expensive collaborator — an external payment gateway or an email-sending service, for instance.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class OrderIntegrationTest { @Autowired private TestRestTemplate restTemplate; @MockBean private PaymentGatewayClient paymentClient; // prevents real HTTP calls @Test void checkout_callsPaymentGateway() { given(paymentClient.charge(any())).willReturn(new ChargeResult("ch_ok", true)); ResponseEntity<String> response = restTemplate.postForEntity("/api/checkout", checkoutRequest(), String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(paymentClient).charge(any()); } }
@MockBean causes context reload. Each unique combination of @MockBean annotations forces Spring to build a new ApplicationContext. If ten test classes each mock a different set of beans, you get ten contexts and your build slows dramatically. Group tests that need the same mocks into the same class, or consider a shared base class that declares the common @MockBean fields.

Stubbing Behaviour and Verifying Interactions

The mock produced by @MockBean is a standard Mockito mock. By default every method returns a "zero value" (null for objects, 0 for numbers, an empty collection for collection return types when using RETURNS_EMPTY settings). Stub only what the test actually exercises:

// Stub a specific call given(orderService.getById(99L)).willReturn(new OrderDto(99L, "SHIPPED", 120.00)); // Stub to throw given(orderService.getById(0L)).willThrow(new OrderNotFoundException(0L)); // Verify the mock was called (optional — only assert observable behaviour) verify(orderService, times(1)).getById(99L); verifyNoMoreInteractions(orderService);

@SpyBean — When You Need the Real Implementation

Sometimes you want the real bean but need to stub or verify one specific method. @SpyBean wraps the real Spring bean in a Mockito spy. All methods execute normally unless you stub them with doReturn(…).when(spy).method(). Use it sparingly — a spy that stubs most of its methods is really just a mock in disguise and signals a design smell.

@SpyBean private AuditService auditService; // real implementation runs @Test void deleteOrder_auditsTheDeletion() { // partial stub: override only the part that hits a network doReturn(true).when(auditService).isEnabled(); restTemplate.delete("/api/orders/5"); verify(auditService).record("DELETE", 5L); }
Do not mix @MockBean and Mockito.mock() for the same type in one test. The field annotated with @MockBean is what Spring injects; a bare Mockito.mock() floating in the test class will never be wired anywhere and stubs on it will silently have no effect.

Context Caching and Performance Impact

Spring Test caches application contexts by their configuration key. @MockBean participates in that key. Practical guidelines to keep the build fast:

  • Declare all @MockBean/@SpyBean fields in a shared abstract base class that your slice tests extend. All subclasses share one context.
  • Prefer @WebMvcTest slices over full @SpringBootTest when you only need the web layer — slice contexts are much cheaper to start.
  • Use @MockBean only for beans the class under test actually depends on. Mocking beans that are not needed "just in case" pollutes the key and fragments the cache.

Summary

@MockBean is the bridge between Mockito's mocking power and Spring's dependency injection. It registers a mock as a first-class Spring bean, allowing slice and integration tests to isolate expensive or external collaborators without losing the benefit of the full wiring framework. Remember: use @Mock for pure unit tests, @MockBean when Spring must wire the mock, and plan your test class groupings carefully to preserve context caching and keep your build fast.