Testing with @SpringBootTest & MockMvc
Reviewed & published by Brayan K
Spring Boot makes tests first-class: @WebMvcTest slices load just your controllers, MockMvc fires simulated HTTP requests, @MockBean stubs collaborators, and @SpringBootTest wires the whole app for integration checks.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should understand dependency injection (why mocking works) and the basics of REST controllers in Spring. Comfort with interfaces helps for mocking.
What You'll Learn
- The arrange-act-assert pattern in JUnit 5
- @SpringBootTest for full-context integration tests
- @WebMvcTest slices for fast controller tests
- Driving controllers with MockMvc
- Replacing beans with @MockBean (Mockito)
- Asserting status and JSON with jsonPath
The Big Idea — Test the Engine on a Bench, Not Just the Whole Car
💡 Analogy: A car maker doesn't crash-test a whole car for every tiny change. They put the engine on a test bench (a slice), feed it controlled inputs (a mock fuel supply), and measure the output — fast and focused. @WebMvcTest + MockMvc is that bench for your controllers: it spins up just the web layer and lets you fire fake requests at it, with @MockBean standing in for the rest of the engine. Occasionally you still do a full road test (@SpringBootTest) to confirm everything works together — but you don't do it for every screw you turn.
Small, fast slice tests for most cases; a few full integration tests for the whole-car confidence.
1️⃣ The Foundation — Arrange, Act, Assert
Every test, Spring or not, follows the same shape: arrange the inputs, act by calling the code, and assert the result. JUnit 5 gives you @Test and assertEquals; here's the mindset in plain Java.
2️⃣ Isolating with Mocks — the @MockBean Idea
To test one component without its real dependencies, you replace a collaborator with a mock you control. In Spring that's @MockBean; in plain Java we just inject a stub implementation.
import java.util.*;
public class Main {
// @MockBean replaces a real collaborator with a stub you control. Here we
// hand-build a fake repository (a 'mock') to test a service in isolation —
// exactly what @MockBean does for you inside a Spring test context.
interface UserRepository { Optional<String> findNameById(int id); }
static class UserService {
private final UserRepository repo;
UserService(UserRepository repo) { this.repo = repo; }
String displayName(int id) {
return repo.findNameById(id).map(n -> "User: " + n).orElse("Unknown");
}
}
public static void main(String[] args) {
// Stub the mock: when asked for id 7, return "Ada"; otherwise empty.
UserRepository mock = id -> id == 7 ? Optional.of("Ada") : Optional.empty();
UserService service = new UserService(mock);
System.out.println(service.displayName(7)); // hits the stubbed value
System.out.println(service.displayName(99)); // falls through to default
}
}3️⃣ Slice Tests — @WebMvcTest + MockMvc
@WebMvcTest loads only the web layer. MockMvc sends simulated HTTP requests, and @MockBean provides the controller's service. You assert status and JSON with fluent matchers.
mockMvc.perform(get("/users/7"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Ada"));// SPRING BOOT test of the WEB layer only (needs spring-boot-starter-test).
// @WebMvcTest loads a SLICE: just the controller + MVC infrastructure, NOT
// services or repositories — so it starts fast and stays focused.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // perform simulated HTTP requests
@MockBean
private UserService userService; // the controller's dependency, mocked
@Test
void returnsUserAsJson() throws Exception {
// Arrange: stub the mocked service.
Mockito.when(userService.findById(7))
.thenReturn(new User(7, "Ada"));
// Act + Assert: send a request, check status and JSON body.
mockMvc.perform(get("/users/7"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(7))
.andExpect(jsonPath("$.name").value("Ada"));
}
@Test
void missingUserReturns404() throws Exception {
Mockito.when(userService.findById(99))
.thenThrow(new UserNotFoundException());
mockMvc.perform(get("/users/99"))
.andExpect(status().isNotFound());
}
}4️⃣ Full Integration — @SpringBootTest
When you need real beans wired together, @SpringBootTest boots the whole context. It's slower, so reserve it for end-to-end confidence rather than every test.
// FULL integration test: @SpringBootTest loads the ENTIRE application context,
// wiring real beans together. Useful for end-to-end behaviour, but slower.
@SpringBootTest
class OrderFlowIntegrationTest {
@Autowired
private OrderService orderService; // a REAL, fully-wired bean
@Test
void placingAnOrderReducesStock() {
long before = orderService.stockOf("widget");
orderService.place("widget", 3);
// Assert against the real, wired-together collaborators.
assertEquals(before - 3, orderService.stockOf("widget"));
}
}🧠 Quick Recall
Predict before you peek.
Answer: @SpringBootTest loads the full context; @WebMvcTest loads only the web-layer slice.
Answer: a bean in the test context with a Mockito mock you can stub and verify.
Answer: no — it dispatches simulated requests through the mock servlet environment, no port opened.
🎯 YOUR TURN — Write Two Assertions
Fill in two check(...) calls so both pass — the arrange-act-assert rhythm.
import java.util.*;
public class Main {
// YOUR TURN: write two assertions (arrange/act/assert) for a Calculator.
// Use the provided check() helper. Make both pass.
//
// Expected output:
// PASS adds
// PASS multiplies
static void check(boolean cond, String name) {
System.out.println((cond ? "PASS " : "FAIL ") + name);
}
static class Calculator {
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
}
public static void main(String[] args) {
Calculator calc = new Calculator();
// check( ..., "adds"); // assert calc.add(2,3) == 5
// check( ..., "multiplies"); // assert calc.mul(4,5) == 20
}
}🧩 MINI-CHALLENGE — Stub and Verify
Build a recording mock for a NotificationGateway, inject it, then verify the interactions.
import java.util.*;
import java.util.function.*;
public class Main {
// MINI-CHALLENGE: simulate @MockBean for a NotificationGateway. Build a mock
// that RECORDS the messages it was asked to send (instead of really sending),
// inject it into a service, then VERIFY the interaction — the two halves of
// mock-based testing: stubbing and verification.
//
// Expected output:
// sent count: 2
// verified "welcome ana"? true
interface NotificationGateway { void send(String msg); }
static class SignupService {
private final NotificationGateway gateway;
SignupService(NotificationGateway g) { this.gateway = g; }
void signup(String user) { gateway.send("welcome " + user); }
}
public static void main(String[] args) {
// List<String> sent = new ArrayList<>();
// NotificationGateway mock = sent::add; (records each send)
// build the service, call signup twice, then print count and a verify
}
}Common Errors
- ❌ Using @SpringBootTest for everything. Slow suites discourage testing. Fix: prefer slices like @WebMvcTest.
- ❌ Forgetting @MockBean in a slice. The context fails to start without the controller's service. Fix: mock missing collaborators.
- ❌ Mixing JUnit 4 and 5 annotations. org.junit.Test won't run under Jupiter. Fix: import from org.junit.jupiter.api.
- ❌ Not stubbing the mock. An un-stubbed Mockito mock returns null/0. Fix: set when(...).thenReturn(...).
- ❌ Asserting only status, not body. You miss serialization bugs. Fix: add jsonPath checks.
- ❌ Sharing mutable state between tests. Causes flaky order-dependent failures. Fix: reset state in @BeforeEach.
Pro Tips
- 💡 Keep the test pyramid: many unit/slice tests, fewer full integration tests.
- 💡 Use AssertJ's assertThat for readable, fluent assertions.
- 💡 Name tests for behaviour (e.g. missingUserReturns404) so failures read like a spec.
- 💡 Verify interactions with Mockito.verify(...) when the side effect matters, not just the return value.
📋 Quick Reference
| Concept | Syntax / idea | Note |
|---|---|---|
| test engine | JUnit 5 @Test | org.junit.jupiter.api |
| full context | @SpringBootTest | Integration, slower |
| web slice | @WebMvcTest(X.class) | Controllers only |
| drive requests | mockMvc.perform(get(...)) | No real server |
| mock a bean | @MockBean | Mockito mock |
| stub | when(...).thenReturn(...) | Mockito |
| status assert | andExpect(status().isOk()) | HTTP 200 |
| body assert | jsonPath("$.name").value(...) | JSON field |
| setup | @BeforeEach | Per test |
| verify call | Mockito.verify(mock) | Interaction check |
📚 Resources / Keep Learning
- 📘 Testing — Spring Boot reference
- 📗 JUnit 5 User Guide
- 📗 Testing the Web Layer — Spring guide
🎉 Lesson Complete!
You can now write arrange-act-assert tests in JUnit 5, slice the web layer with @WebMvcTest, drive controllers via MockMvc, stub collaborators with @MockBean, and boot the full app with @SpringBootTest when you need it.
Practice quiz
What does @SpringBootTest do?
- Loads the full Spring application context for integration testing
- Runs only static analysis
- Compiles the code faster
- Deletes the database
Answer: Loads the full Spring application context for integration testing. @SpringBootTest bootstraps the complete ApplicationContext so you can test wired-together beans end to end.
What is @WebMvcTest used for?
- Loading the entire context
- Testing only the web layer (controllers) with a sliced context
- Testing the database only
- Running JavaScript
Answer: Testing only the web layer (controllers) with a sliced context. @WebMvcTest is a test slice that loads just the MVC components (controllers, filters) and not services or repositories.
What is MockMvc?
- A mock database driver
- A JSON library
- A way to perform HTTP requests against controllers without a running server
- A logging framework
Answer: A way to perform HTTP requests against controllers without a running server. MockMvc lets you send simulated HTTP requests to your controllers and assert on the response, with no real servlet container.
What does @MockBean do in a Spring test?
- Starts a real bean
- Deletes a bean permanently
- Encrypts a bean
- Replaces a bean in the context with a Mockito mock
Answer: Replaces a bean in the context with a Mockito mock. @MockBean adds or replaces a bean in the test ApplicationContext with a Mockito mock you can stub and verify.
Which testing framework does Spring Boot use by default?
- JUnit 3
- TestNG only
- JUnit 5 (Jupiter)
- Selenium
Answer: JUnit 5 (Jupiter). Spring Boot's spring-boot-starter-test brings JUnit 5 (Jupiter) as the default test engine.
Which annotation marks a JUnit 5 test method?
- @TestMethod
- @Test (org.junit.jupiter.api.Test)
- @RunTest
- @Spec
Answer: @Test (org.junit.jupiter.api.Test). In JUnit 5 a test is a method annotated with @Test from org.junit.jupiter.api.
Why prefer @WebMvcTest over @SpringBootTest for a controller unit test?
- It loads a smaller, faster slice of the context
- It tests the database too
- It encrypts requests
- It is required by Java 21
Answer: It loads a smaller, faster slice of the context. Loading only the web layer makes the test start faster and stay focused on controller behaviour.
With MockMvc, how do you assert the HTTP status is 200 OK?
- status().isError()
- expect(200)
- andExpect(status().isOk())
- assertHttp(ok)
Answer: andExpect(status().isOk()). perform(...).andExpect(status().isOk()) checks the simulated response returned a 200 status.
When you use @WebMvcTest, what happens to the controller's service dependency?
- You provide it as a @MockBean since services aren't in the slice
- It is auto-instantiated for real
- It is ignored
- It must be static
Answer: You provide it as a @MockBean since services aren't in the slice. The web slice doesn't load services, so you supply collaborators as @MockBean and stub their behaviour.
What does andExpect(jsonPath("$.name").value("Ada")) verify?
- The request header name
- The database column type
- That the JSON response field 'name' equals 'Ada'
- The server port
Answer: That the JSON response field 'name' equals 'Ada'. jsonPath navigates the JSON response body; here it asserts the 'name' field equals 'Ada'.
Continue this course
- Previous: JWT Authentication
- Next: Testing the Data Layer — @DataJpaTest, test slices and Testcontainers
- Quick reference: Java cheat sheet
Frequently asked questions
What is the difference between @SpringBootTest and @WebMvcTest?
@SpringBootTest loads the entire Spring ApplicationContext — every bean, configuration, and auto-configuration — which is ideal for integration tests that exercise wired-together components, but is comparatively slow. @WebMvcTest is a 'test slice' that loads only the web layer: your controllers plus MVC infrastructure like JSON serialization and exception handling, and nothing from the service or persistence layers. Use @WebMvcTest for fast, focused controller tests (supplying collaborators as @MockBean), and @SpringBootTest when you genuinely need the whole application wired up.
How does MockMvc work without a running server?
MockMvc dispatches simulated HTTP requests directly through Spring's DispatcherServlet and the mock servlet environment, never opening a real network port. You write mockMvc.perform(get("/users/7")) to send a request and then chain andExpect(...) matchers to assert on the status, headers, and JSON body. Because there's no real server, these tests are fast and deterministic, while still exercising routing, argument binding, validation, and serialization the same way a real request would.
When and why do I use @MockBean?
@MockBean replaces (or adds) a bean in the test's ApplicationContext with a Mockito mock. It's most useful in slice tests like @WebMvcTest, where the controller depends on a service that isn't loaded by the slice: you declare the service as a @MockBean, stub its methods with Mockito.when(...).thenReturn(...), and the controller calls your stub. This isolates the layer under test, lets you simulate edge cases (such as a service throwing an exception), and lets you verify how the controller used the dependency.
Which JUnit version does Spring Boot use, and how do I write a test?
Modern Spring Boot uses JUnit 5 (also called JUnit Jupiter), bundled via spring-boot-starter-test along with Mockito, AssertJ, and Spring Test. A test is a method annotated with @Test from org.junit.jupiter.api inside a test class. You typically follow the arrange-act-assert pattern: set up inputs and stubs, invoke the behaviour, then assert on the result using assertions like assertEquals or AssertJ's fluent assertThat. Lifecycle annotations such as @BeforeEach and @AfterEach run setup and teardown around each test.
Should I prefer slice tests or full-context tests?
Favour the smallest test that gives you confidence. Slice tests (@WebMvcTest, @DataJpaTest, and friends) load only the relevant part of the application, so they start fast and pinpoint failures to one layer — ideal for the bulk of your suite. Reserve @SpringBootTest integration tests for verifying that the layers actually work together correctly, since those are slower and broader. A healthy suite is mostly fast slice and unit tests with a smaller number of full integration tests covering critical end-to-end paths.