12.3 테스트 전략: 단위·컨트롤러·통합 테스트
전문 개발 환경에서는 단위 테스트, 컨트롤러(API) 테스트, 통합 테스트를 구분하여 작성합니다. 스프링 부트는 JUnit 5, Mockito, MockMvc, Testcontainers 등과 잘 통합되어 있습니다.
작성 기준: Spring Boot 3.2.x, JUnit 5, Mockito
1. 테스트 계층 정리
| 유형 | 목적 | 사용 도구 | 특징 |
|---|---|---|---|
| 단위 테스트 | Service 등 단일 클래스 로직 검증 | JUnit 5, Mockito | 빠름, 외부 의존성 Mock |
| 컨트롤러 테스트 | HTTP 요청/응답·직렬화·Validation 검증 | @WebMvcTest, MockMvc | 웹 계층만 로드, Service Mock |
| 통합 테스트 | DB·전체 스택 동작 검증 | @SpringBootTest, Testcontainers | 느리지만 실제에 가까움 |
2. 단위 테스트 (Service)
의존하는 Repository·외부 클라이언트는 @Mock으로 대체하고, 테스트 대상만 @InjectMocks로 주입합니다.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock ProductRepository productRepository;
@InjectMocks OrderService orderService;
@Test
@DisplayName("재고 부족 시 예외 발생")
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. 컨트롤러 테스트 (@WebMvcTest + MockMvc)
컨트롤러만 띄우고 Service는 Mock으로 넣어, HTTP 요청·응답 코드·JSON 구조만 검증할 때 사용합니다. 전체 컨텍스트를 띄우지 않아 빠릅니다.
@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): 해당 컨트롤러와 Web 레이어만 로드.@MockBean: 컨텍스트에 Mock Service를 빈으로 등록.MockMvc: 실제 HTTP 서버 없이 요청/응답 시뮬레이션.
4. JPA/Repository 테스트 (@DataJpaTest)
JPA·Repository만 띄우고, 기본적으로 인메모리 DB(H2)로 동작합니다. 실제 DB 스키마를 쓰려면 Testcontainers와 조합합니다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // 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. 통합 테스트 (@SpringBootTest)
전체 애플리케이션 컨텍스트와 DB를 띄워, API부터 DB까지 한 번에 검증합니다. 실제 DB 대신 Testcontainers로 MySQL/PostgreSQL 컨테이너를 띄우면 환경 일관성이 좋습니다.
@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. 요약
- Service 로직:
@ExtendWith(MockitoExtension.class)+@Mock/@InjectMocks. - API 계약·Validation:
@WebMvcTest+MockMvc+@MockBean. - Repository·JPA:
@DataJpaTest(H2 또는 Testcontainers). - 전체 플로우:
@SpringBootTest+ Testcontainers +TestRestTemplate.
이렇게 계층별로 테스트를 나누면 실패 시 원인 파악이 쉽고, CI에서도 빠른 단위/컨트롤러 테스트와 느린 통합 테스트를 분리해서 실행할 수 있습니다.