Skip to main content
Advertisement

12.3 Test Strategy: Unit, Controller, and Integration Tests

In professional setups, unit, controller (API), and integration tests are clearly separated. Spring Boot integrates well with JUnit 5, Mockito, MockMvc, and Testcontainers.

Reference: Spring Boot 3.2.x, JUnit 5, Mockito

1. Test Layers Overview

TypePurposeToolsNotes
UnitValidate single-class logic (e.g. Service)JUnit 5, MockitoFast; external deps mocked
ControllerValidate HTTP request/response, serialization, validation@WebMvcTest, MockMvcWeb layer only; Service mocked
IntegrationValidate DB and full stack@SpringBootTest, TestcontainersSlower but realistic

2. Unit Tests (Service)

Mock dependencies (Repository, external clients) with @Mock and inject the target with @InjectMocks.

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

@Mock OrderRepository orderRepository;
@Mock ProductRepository productRepository;
@InjectMocks OrderService orderService;

@Test
@DisplayName("Throws when stock is insufficient")
void order_throwsWhenStockInsufficient() {
Product product = new Product("item1", 10);
when(productRepository.findByName("item1")).thenReturn(Optional.of(product));

assertThrows(BusinessException.class,
() -> orderService.createOrder(new OrderDto("item1", 100)));
}
}

3. Controller Tests (@WebMvcTest + MockMvc)

Load only the controller and mock the Service to verify HTTP status, headers, and JSON. Avoids full context startup.

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired MockMvc mockMvc;
@MockBean UserService userService;

@Test
void getUser_returns200() throws Exception {
when(userService.findById(1L))
.thenReturn(new UserResponseDto(1L, "user@test.com", "User"));

mockMvc.perform(get("/api/users/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(1))
.andExpect(jsonPath("$.data.email").value("user@test.com"));
}

@Test
void createUser_validatesRequestBody() throws Exception {
String invalidJson = "{\"email\":\"not-an-email\", \"name\":\"\"}";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJson))
.andExpect(status().isBadRequest());
}
}
  • @WebMvcTest(UserController.class): Loads only that controller and web layer.
  • @MockBean: Registers a mock Service in the context.
  • MockMvc: Simulates HTTP without a real server.

4. JPA/Repository Tests (@DataJpaTest)

Runs only JPA and repositories, typically against an in-memory DB (e.g. H2). Use Testcontainers for a real DB schema.

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // when using Testcontainers
class OrderRepositoryTest {

@Autowired OrderRepository orderRepository;

@Test
void findByStatus_returnsFilteredList() {
orderRepository.save(new Order("item1", OrderStatus.ORDER));
orderRepository.save(new Order("item2", OrderStatus.CANCEL));

List<Order> orders = orderRepository.findByStatus(OrderStatus.ORDER);
assertThat(orders).hasSize(1).first().extracting(Order::getItemName).isEqualTo("item1");
}
}

5. Integration Tests (@SpringBootTest)

Starts the full application context and DB. Use Testcontainers for MySQL/PostgreSQL so all environments stay consistent.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderApiIntegrationTest {

@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test").withUsername("test").withPassword("test");

@DynamicPropertySource
static void setDatasource(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}

@Autowired TestRestTemplate restTemplate;

@Test
void orderFlow_createsOrderAndDecrementsStock() {
ResponseEntity<ApiResponse<OrderResponseDto>> res = restTemplate.postForEntity(
"/api/orders",
new OrderRequestDto("item1", 2),
new ParameterizedTypeReference<>() {});
assertThat(res.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(res.getBody().getData().getItemName()).isEqualTo("item1");
}
}

6. Summary

  • Service logic: @ExtendWith(MockitoExtension.class) + @Mock / @InjectMocks.
  • API contract and validation: @WebMvcTest + MockMvc + @MockBean.
  • Repository and JPA: @DataJpaTest (H2 or Testcontainers).
  • Full flow: @SpringBootTest + Testcontainers + TestRestTemplate.

Separating tests by layer makes failures easier to diagnose and allows CI to run fast unit/controller tests separately from slower integration tests.

Advertisement