Testing controllers in Spring Boot can feel like a balancing act.
You want fast, focused tests that catch real-world issues—without slowing down your suite or missing critical behavior. Too often, developers fall into one of two traps: writing plain unit tests with Mockito and JUnit, or reaching for the full-blown @SpringBootTest
.
Both miss the mark. In this post, I’ll show you why – and how @WebMvcTest
strikes the perfect balance, preserving HTTP semantics while keeping your tests lean and powerful.
The Pitfalls of Plain Unit Tests
Let’s start with the common go-to: a plain unit test for your controller using JUnit and Mockito. It’s tempting – mock out the service layer, call your controller method directly, and assert the result. Quick, isolated, done. Right?
Not quite. When you test a controller this way, you lose something critical: HTTP semantics. Controllers in Spring Boot aren’t just random classes—they’re the entry point to your web application, handling requests, responses, status codes, headers, and security.
Bypassing the HTTP layer with a plain unit test means you’re not testing how your controller behaves in the real world.
Here’s what you miss:
- Request Mapping: Does
/api/users/{id}
actually resolve to your method? - Status Codes: Will a bad request return a 400 or an accidental 200?
- Headers: Are you setting Content-Type or custom headers correctly?
- Security: Is your
@PreAuthorize
rule enforced?
Mocking everything with Mockito might verify your method logic, but it skips the glue that makes a controller a controller. You’re testing a hollow shell, not the real thing.
Why @SpringBootTest Is Overkill
On the flip side, some developers swing too far the other way: @SpringBootTest
. This annotation spins up your entire Spring Boot application – database, services, beans, everything.
Sure, it tests your controller in a “real” environment, complete with HTTP semantics via a TestRestTemplate
or WebTestClient
. But it’s like using a sledgehammer to crack a walnut.
Here’s the problem:
- Slow Startup: Loading the full application context takes 20–45 seconds (or more), slowing your test suite.
- Unnecessary Scope: You’re testing the controller, not the database or unrelated beans.
- Complexity: Debugging failures becomes a needle-in-a-haystack hunt across layers.
@SpringBootTest
is great for integration tests where you need the whole app, but for controller testing? It’s overkill. You want focus, speed, and just enough realism – without the baggage.
The Sweet Spot: @WebMvcTest
Enter @WebMvcTest
– the go-to solution for testing Spring Boot controllers.
This annotation loads only the web layer (controllers, filters, and MVC infrastructure) while mocking the rest of your app. It’s fast, focused, and – crucially – preserves HTTP semantics through Spring’s MockMvc.
Here’s why it’s the best choice:
1. Mock HTTP Semantics
With MockMvc, you simulate real HTTP requests – GET, POST, PUT, DELETE, you name it.
You’re not calling methods directly; you’re hitting endpoints like a client would.
This lets you verify:
- URL Mappings: Does
/api/users
route correctly? - Request Payloads: Are JSON inputs deserialized properly?
- Response Bodies: Is the output what you expect?
2. Status Codes and Headers
MockMvc gives you full control to check HTTP-specific details:
- Status:
expect(status().isOk())
orexpect(status().isForbidden())
. - Headers:
expect(header().string("X-Custom-Header", "value"))
. - Content Type:
expect(content().contentType(MediaType.APPLICATION_JSON))
.
No more guessing if your controller sets a 201 Created or forgets a header – @WebMvcTest
has you covered.
3. Security Testing
Spring Security enabled?
@WebMvcTest
integrates seamlessly. Add .with(user("testuser").roles("USER"))
to your request and test:
Authentication: Does the endpoint reject unauthenticated calls?
Authorization: Are roles like @PreAuthorize("hasRole('ADMIN')")
enforced?
You can’t do that with a plain unit test—and @SpringBootTest
makes it painfully slow.
4. Speed and Focus
By loading only the MVC layer and mocking dependencies (e.g., services with @MockitoBean
), @WebMvcTest
keeps tests lightweight. No database connections, no unrelated beans – just the controller and its HTTP behavior.
A Quick Example
Let’s see @WebMvcTest
in action. Imagine a simple controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { User user = userService.findById(id); return ResponseEntity.ok(user); } } |
Here’s how to test it with @WebMvcTest
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockitoBean private UserService userService; @Test void getUser_ReturnsUser_WhenFound() throws Exception { // Arrange User user = new User(1L, "Alice"); when(userService.findById(1L)).thenReturn(user); // Act & Assert mockMvc.perform(get("/api/users/1") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.name").value("Alice")); } } |
What’s happening here?
@WebMvcTest loads
onlyUserController
and MVC components.@MockitoBean
mocksUserService
, keeping the test isolated.MockMvc
sends a GET request to/api/users/1
and checks the status, content type, and JSON response.
Fast, focused, and HTTP-aware. No full app startup, no bypassing the web layer.
Tips for @WebMvcTest Success
- Mock Dependencies: Use
@MockBean
for services or repositories your controller needs. - Security Setup: Add
@WithMockUser
or customSecurityMockMvcRequestPostProcessors
for auth tests. - Error Handling: Test 400, 404, or 500 responses with invalid inputs or mock failures.
- Keep It Lean: Avoid loading unnecessary controllers by specifying the target class (e.g.,
@WebMvcTest(UserController.class)
).
Wrap-Up: Test Smarter, Not Harder
Testing Spring Boot controllers doesn’t have to be a compromise.
Plain unit tests with Mockito strip away HTTP semantics, leaving gaps in your coverage. @SpringBootTest
drowns you in overhead you don’t need. @WebMvcTest
hits the sweet spot – mock HTTP, real endpoints, and just enough context to test what matters: status codes, headers, security, and responses.
Next time you write a controller test, skip the extremes and reach for @WebMvcTest
. Your test suite will thank you – and so will your team.
Want to dive deeper into Spring Boot testing? The Testing Spring Boot Applications Masterclass covers this and more – advanced techniques, best practices, and real-world examples. Ready to level up?
Become a more productive and confident Spring Boot developer.
Joyful testing,
Phil