Should I Use Unit Tests or Integration Tests for Spring Boot REST Controllers?
Problem
I’m testing my Spring Boot REST controllers, but I’m confused about whether to use @WebMvcTest or @SpringBootTest. Some of my tests are slow, and I’m not sure what I’m actually testing.
// Which one should I use?@WebMvcTest(UserController.class)@SpringBootTestEnvironment
- Spring Boot 3.x
- JUnit 5
- Mockito
- MockMvc / TestRestTemplate
What happened?
I wrote tests without understanding the difference between @WebMvcTest and @SpringBootTest.
First, I used @WebMvcTest but expected database queries to work:
@WebMvcTest(UserController.class)class UserControllerTest { @Autowired private MockMvc mockMvc;
@Test void shouldReturnUser() throws Exception { // This test never hits the database! // @WebMvcTest doesn't load @Repository beans mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()); }}The test failed because @WebMvcTest only loads the web layer. My repository wasn’t even instantiated.
Then I switched to @SpringBootTest for everything:
@SpringBootTestclass UserValidationTest { @Autowired private TestRestTemplate restTemplate;
@Test void shouldRejectInvalidEmail() { // Takes 10+ seconds just to check email validation // Could be done in 1 second with @WebMvcTest }}Now my test suite takes 5 minutes to run because every test loads the full Spring context.
How to solve it?
I learned to match the test type to what I’m actually validating.
Use @WebMvcTest for controller layer only
@WebMvcTest slices the Spring context to load only web components:
@Controllerand@ControllerAdvice@JsonComponent- Web MVC configuration
It does NOT load:
@Service,@Component,@Repositorybeans- Database configuration
- Security filters (unless explicitly included)
@WebMvcTest(UserController.class)class UserControllerWebMvcTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserService userService; // Must mock - service isn't loaded
@Test void shouldReturnUserById() throws Exception { // Arrange: mock the service when(userService.findById(1L)).thenReturn(user);
// Act & Assert: test the controller layer mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("John Doe"));
verify(userService).findById(1L); }
@Test void shouldRejectInvalidEmail() throws Exception { String invalidUser = """ { "email": "not-an-email", "name": "John Doe" } """;
mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(invalidUser)) .andExpect(status().isBadRequest()); }}What this tests:
- Controller receives request correctly
- JSON path expressions work
- Validation annotations are applied
- HTTP status codes are correct
What this does NOT test:
- Service layer logic (it’s mocked)
- Database queries
- Real JSON serialization edge cases
Use @SpringBootTest for full integration
@SpringBootTest loads the entire application context:
- All services with real logic
- Database connections and queries
- Security filters
- All Spring configuration
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)@Transactionalclass UserControllerIntegrationTest {
@Autowired private TestRestTemplate restTemplate;
@Autowired private UserRepository userRepository;
@Test void shouldCreateAndRetrieveUser() { // Arrange: no mocking, real database CreateUserRequest request = new CreateUserRequest( "John Doe" );
// Act: full HTTP request through all layers ResponseEntity<UserResponse> createResponse = restTemplate.postForEntity( "/api/users", request, UserResponse.class );
// Assert: verify full integration assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
// Verify database state assertThat(saved).isNotNull(); }
@Test void shouldHandleUniqueConstraintViolation() { // Create first user
// Try to create duplicate CreateUserRequest request = new CreateUserRequest( "Duplicate" );
ResponseEntity<ErrorResponse> response = restTemplate.postForEntity( "/api/users", request, ErrorResponse.class );
// Test real error handling across layers assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); }}What this tests:
- Full request/response cycle
- Real JSON serialization with your ObjectMapper
- Actual database queries and constraints
- Security filters (if configured)
- Exception handling across layers
The reason
The key difference is what each test type loads:
┌─────────────────────────────────────────────────────────────┐│ @WebMvcTest │├─────────────────────────────────────────────────────────────┤│ ││ Loads: Controller, ControllerAdvice, JSON components ││ Mocks: Service, Repository, Database ││ Speed: ~1-3 seconds ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ MockMvc │ ──▶ │Controller│ ──▶ │@MockBean│ ││ └─────────┘ └─────────┘ │ Service │ ││ └─────────┘ │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ @SpringBootTest │├─────────────────────────────────────────────────────────────┤│ ││ Loads: Everything - Controllers, Services, DB, Security ││ Mocks: Nothing (or selective) ││ Speed: ~5-15 seconds ││ ││ ┌──────────────────┐ ┌─────────┐ ┌─────────┐ ││ │ TestRestTemplate │ ──▶ │Controller│ ──▶ │ Service │ ││ └──────────────────┘ └─────────┘ └────┬────┘ ││ │ ││ ▼ ││ ┌─────────┐ ││ │Database │ ││ └─────────┘ │└─────────────────────────────────────────────────────────────┘Decision matrix
| Scenario | Recommended Test | Reason |
|---|---|---|
Validating @Valid annotations | @WebMvcTest | Fast, isolated |
| Testing exception handlers | @WebMvcTest | ControllerAdvice is loaded |
| Verifying JSON field names | @WebMvcTest | Quick feedback |
| Testing actual queries execute | @SpringBootTest | Need real database |
| Verifying security config | @SpringBootTest | Filters must load |
| Testing error handling across layers | @SpringBootTest | Need full stack |
| CI/CD fast feedback | @WebMvcTest | Speed matters |
Common mistakes
Mistake 1: Using @WebMvcTest but expecting database queries
@WebMvcTest(UserController.class)class UserControllerTest { // @Repository beans are NOT loaded! // You MUST use @MockBean for repositories}Mistake 2: Using @SpringBootTest for simple validation tests
// TOO SLOW - loading full context for validation test@SpringBootTestclass EmailValidationTest { // Could use @WebMvcTest in 1 second instead of 10 seconds}Mistake 3: Not understanding what @MockBean does
@WebMvcTest(OrderController.class)class OrderControllerTest { @MockBean private OrderService orderService; // Returns null by default!
@Test void shouldCreateOrder() { // If you don't mock the return value, // orderService.createOrder() returns null }}Mistake 4: Mixing both in the same test class
Don’t mix @WebMvcTest and @SpringBootTest in the same class. They’re fundamentally different approaches.
Recommended strategy
- Write
@WebMvcTestfor every controller - Test routing, validation, response structure - Write
@SpringBootTestfor critical paths - Happy paths, complex flows, error scenarios - Use
@MockBeansparingly in integration tests - Defeats the purpose - Run
@WebMvcTeston every commit - Fast feedback - Run
@SpringBootTestbefore merge - Comprehensive validation
Summary
In this post, I explained when to use @WebMvcTest vs @SpringBootTest for testing REST controllers. The key point is to match the test type to what you’re validating. Use @WebMvcTest for fast, isolated tests of controller mechanics. Use @SpringBootTest when you need to verify the full request-response cycle including database and serialization. The common mistake is not understanding the difference—not choosing wrong, but choosing without knowing.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Framework Documentation: Testing the Web Layer
- 👨💻 Spring Boot Reference: @WebMvcTest
- 👨💻 Spring Boot Reference: @SpringBootTest
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments