“A @SpringBootTest
is like testing your entire car—engine, wheels, and stereo system. A @WebMvcTest
is like putting just the engine’s control panel on a test bench. It’s much faster because you’re only testing the web layer. 🏎️”
When you’re building a robust Spring Boot application, integration tests are your safety net. But running a full @SpringBootTest
for every little change in your web layer can feel like starting up the entire car just to check if the radio works. It loads the full application context, connects to the database, and initializes every bean. It’s thorough, but it can be slow.
What if you only want to test the controller? To check if it handles requests correctly, performs validation, and returns the right HTTP status codes? Enter @WebMvcTest
, the perfect tool for slicing your application and testing just the web layer in isolation. Let’s put your controller on the test bench and see how fast it can go!
Prerequisites
- A Spring Boot project with a simple REST controller.
- Basic understanding of JUnit and Mockito.
- Maven or Gradle configured in your project.
1️⃣ Full System vs. Sliced Test: @SpringBootTest
vs. @WebMvcTest
Before we dive in, let’s clarify the difference between these two powerful testing annotations. Think of it as testing the whole car versus just one critical part.
Feature | @SpringBootTest (The Whole Car) |
@WebMvcTest (The Control Panel) |
---|---|---|
Scope | Loads the complete Spring ApplicationContext. | Loads only the web layer beans (Controllers, Filters, etc.). |
Components Loaded | @Controller , @Service , @Repository , etc. (everything). |
Only @Controller and MVC infrastructure. Services and Repositories are NOT loaded. |
Speed | Slower, as it initializes the entire application. | Much faster, as it only configures a fraction of the app. |
Use Case | Full end-to-end integration tests. | Testing controller logic, request mapping, validation, and JSON serialization. |
By slicing our test to focus only on the web layer, we get faster feedback, which is crucial for a smooth development workflow.
2️⃣ Setting Up the Test Bench: Your First @WebMvcTest
Let’s start with a simple BananaController
that we want to test.
// The Controller we want to test
package com.example.jungleblog.web;
import com.example.jungleblog.service.BananaService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/bananas")
public class BananaController {
private final BananaService bananaService;
public BananaController(BananaService bananaService) {
this.bananaService = bananaService;
}
@GetMapping("/{id}")
public ResponseEntity<String> getBananaById(@PathVariable Long id) {
String banana = bananaService.getBanana(id);
return ResponseEntity.ok(banana);
}
}
// The service dependency (we will mock this)
package com.example.jungleblog.service;
import org.springframework.stereotype.Service;
@Service
public class BananaService {
public String getBanana(Long id) {
// In a real app, this would fetch from a database
return "Golden Banana";
}
}
To set up a test for BananaController
, we create a test class and annotate it with @WebMvcTest
, specifying which controller we want to put on the bench.
package com.example.jungleblog.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(BananaController.class) // <-- 1. Target the controller to test
class BananaControllerTest {
@Autowired
private MockMvc mockMvc; // <-- 2. The tool to perform HTTP requests
// ... tests will go here ...
}
- 1.
@WebMvcTest(BananaController.class)
: This annotation tells Spring Boot to only configure the MVC infrastructure needed for testingBananaController
. It will not load any@Service
or@Repository
beans. - 2.
MockMvc
: This powerful class, auto-configured by@WebMvcTest
, lets you simulate HTTP requests to your controller without needing a real web server.
3️⃣ Faking the Engine: Mocking Dependencies with @MockBean
If you try to run the test above, it will fail! Why? Because @WebMvcTest
did not load BananaService
. The BananaController
needs a BananaService
bean to be constructed, but there isn’t one in the test context.
This is by design. We don’t want to test the real service logic, just the controller. The solution is to provide a “fake” or mock version of the service using @MockBean
.
package com.example.jungleblog.web;
import com.example.jungleblog.service.BananaService;
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.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(BananaController.class)
class BananaControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // <-- Creates a Mockito mock of BananaService
private BananaService bananaService;
@Test
void whenGetBananaById_thenReturnBananaString() throws Exception {
// ... test logic ...
}
}
The @MockBean
annotation adds a mock implementation of BananaService
to the application context. Spring will see this mock and inject it into the BananaController
instance it creates for the test. Now our test bench is fully wired up!
4️⃣ Running a Simulation: Testing with MockMvc
Now we can write our test. The process is:
- Arrange: Define the behavior of your mocks. Tell the fake
bananaService
what to return when it’s called. - Act: Use
mockMvc
to perform an HTTP request to your controller’s endpoint. - Assert: Check the HTTP response. Was the status code correct? Was the response body what you expected?
// Inside BananaControllerTest.java
@Test
void whenGetBananaById_thenReturnBananaString() throws Exception {
// 1. Arrange: Define the mock's behavior
// "Given the bananaService is called with id 1, then return this specific string."
given(bananaService.getBanana(1L)).willReturn("Mocked Golden Banana");
// 2. Act & 3. Assert
mockMvc.perform(get("/api/bananas/1")) // Perform a GET request
.andExpect(status().isOk()) // Expect HTTP 200 OK
.andExpect(content().string("Mocked Golden Banana")); // Expect the response body to be our mocked string
}
@Test
void whenGetBananaById_withUnknownId_thenNotFound() throws Exception {
// Arrange: Define behavior for a case that should fail (e.g., banana not found)
given(bananaService.getBanana(99L)).willThrow(new RuntimeException("Banana not found"));
// Act & Assert
mockMvc.perform(get("/api/bananas/99"))
.andExpect(status().isInternalServerError()); // Or a more specific status depending on your error handling
}
Running this test class will give you a green checkmark! You’ve successfully tested your controller’s request mapping and response generation without ever touching the real service or database. This is a fast, focused, and reliable way to test your web layer.
💡 Monkey-Proof Tips
- Focus on Controller Responsibilities: Use
@WebMvcTest
to check things like: Does the endpoint URL work? Are path variables and request parameters parsed correctly? Does it handle invalid input with a 400 Bad Request? Is the JSON serialization/deserialization correct? - Don’t Test Service Logic: Notice we never checked how the banana string was generated. We only checked that the controller correctly returned whatever the service gave it. The service logic should be tested in its own separate unit test.
- Test Security Rules:
@WebMvcTest
is excellent for testing security. You can import your security configuration and use MockMvc’s helpers (likewith(user("..."))
) to test if your endpoint correctly enforces authentication and authorization rules.
🚀 Challenge
Time to apply your new slicing skills!
- Find a controller in your project that you are currently testing with a full
@SpringBootTest
. - Create a new test class for that same controller, but this time use
@WebMvcTest
. - Identify all the service-layer dependencies injected into your controller and provide mocks for them using
@MockBean
. - Rewrite one of the tests using
MockMvc
. Define the behavior of your mock beans and assert that the controller responds correctly. - Run both the old
@SpringBootTest
and your new@WebMvcTest
and notice the difference in execution time!
👏 You’ve now mastered the art of the test slice! By using @WebMvcTest
, you can create a suite of fast, reliable, and focused tests for your web layer, leading to a more efficient and confident development process. Keep your tests fast, your feedback instant, and your code monkey-proof!