Skip to content

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)
@SpringBootTest

Environment

  • 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:

UserControllerTest.java
@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:

UserValidationTest.java
@SpringBootTest
class 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:

  • @Controller and @ControllerAdvice
  • @JsonComponent
  • Web MVC configuration

It does NOT load:

  • @Service, @Component, @Repository beans
  • Database configuration
  • Security filters (unless explicitly included)
UserControllerWebMvcTest.java
@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
User user = new User(1L, "[email protected]", "John Doe");
when(userService.findById(1L)).thenReturn(user);
// Act & Assert: test the controller layer
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("[email protected]"))
.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
UserControllerIntegrationTest.java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Transactional
class 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);
assertThat(createResponse.getBody().email()).isEqualTo("[email protected]");
// Verify database state
User saved = userRepository.findByEmail("[email protected]");
assertThat(saved).isNotNull();
}
@Test
void shouldHandleUniqueConstraintViolation() {
// Create first user
userRepository.save(new User("[email protected]", "Existing"));
// 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

ScenarioRecommended TestReason
Validating @Valid annotations@WebMvcTestFast, isolated
Testing exception handlers@WebMvcTestControllerAdvice is loaded
Verifying JSON field names@WebMvcTestQuick feedback
Testing actual queries execute@SpringBootTestNeed real database
Verifying security config@SpringBootTestFilters must load
Testing error handling across layers@SpringBootTestNeed full stack
CI/CD fast feedback@WebMvcTestSpeed 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
@SpringBootTest
class 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.

  1. Write @WebMvcTest for every controller - Test routing, validation, response structure
  2. Write @SpringBootTest for critical paths - Happy paths, complex flows, error scenarios
  3. Use @MockBean sparingly in integration tests - Defeats the purpose
  4. Run @WebMvcTest on every commit - Fast feedback
  5. Run @SpringBootTest before 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments