본문으로 건너뛰기
Advertisement

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에서도 빠른 단위/컨트롤러 테스트와 느린 통합 테스트를 분리해서 실행할 수 있습니다.

Advertisement