@WebMvcTest — 컨트롤러만 잘라서 테스트하는 방법
컨트롤러를 테스트할 때 서비스, 리포지토리, DB까지 전부 띄워야 할까요?
컨트롤러의 책임은 "요청을 받아 서비스에 위임하고, 응답을 반환하는 것"입니다. 서비스 로직이나 DB는 컨트롤러 테스트의 관심사가 아닙니다. @WebMvcTest는 웹 계층만 잘라서 빠르게 테스트할 수 있게 해주는 슬라이스 테스트입니다.
@WebMvcTest란
@WebMvcTest는 Spring Boot의 슬라이스 테스트 어노테이션으로, 웹 계층 관련 빈만 로드 합니다.
로드되는 빈:
@Controller,@RestController@ControllerAdvice,@RestControllerAdvice@JsonComponentFilter,WebMvcConfigurerHandlerMethodArgumentResolver
로드되지 않는 빈:
@Service,@Repository,@Component- 데이터 소스, JPA 관련 설정
@WebMvcTest(ProductController.class) // 특정 컨트롤러만 지정
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // Service를 Mock으로 대체
private ProductService productService;
// 테스트 코드
}
MockMvc 기본 사용법
MockMvc는 서버를 띄우지 않고 DispatcherServlet을 직접 호출하여 HTTP 요청을 시뮬레이션합니다.
GET 요청 테스트
@Test
void 상품_단건_조회_성공() throws Exception {
// given
Product product = new Product(1L, "스프링 인 액션", 35000);
given(productService.findById(1L)).willReturn(product);
// when & then
mockMvc.perform(get("/api/products/{id}", 1L)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("스프링 인 액션"))
.andExpect(jsonPath("$.price").value(35000))
.andDo(print()); // 요청/응답 로그 출력
}
POST 요청 테스트
@Test
void 상품_등록_성공() throws Exception {
// given
ProductCreateRequest request = new ProductCreateRequest("새 상품", 10000);
Product created = new Product(1L, "새 상품", 10000);
given(productService.create(any(ProductCreateRequest.class))).willReturn(created);
// when & then
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "새 상품",
"price": 10000
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("새 상품"));
}
유효성 검증 실패 테스트
@Test
void 상품명_없이_등록하면_400_에러() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "",
"price": 10000
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[0].field").value("name"));
}
@MockBean — 의존성 Mock
@MockBean은 Spring ApplicationContext에 등록된 빈을 Mock 객체로 교체합니다.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@MockBean
private AuthService authService; // Security 관련 서비스도 Mock
@MockBean으로 서비스 의존성을 Mock한 뒤 given()으로 반환값을 설정하고 요청을 검증합니다.
@Test
void 주문_목록_조회() throws Exception {
// given
List<Order> orders = List.of(
new Order(1L, "주문1", 10000),
new Order(2L, "주문2", 20000)
);
given(orderService.findAll()).willReturn(orders);
// when & then
mockMvc.perform(get("/api/orders"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
// 서비스 메서드 호출 검증
then(orderService).should(times(1)).findAll();
}
}
@MockBean vs @Mock
| 구분 | @MockBean | @Mock |
|---|---|---|
| 제공 | Spring Boot Test | Mockito |
| 컨텍스트 | Spring ApplicationContext에 등록 | Mockito 단독 |
| 용도 | 슬라이스/통합 테스트 | 순수 단위 테스트 |
| 성능 | 컨텍스트 캐시 무효화 가능 | 빠름 |
@SpyBean — 부분 Mock
실제 빈의 메서드를 호출하되, 특정 메서드만 Stub하고 싶을 때 사용합니다.
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@SpyBean
private ProductValidator validator; // 실제 로직 수행, 일부만 Stub
@Test
void 검증_로직을_거치는_등록_테스트() throws Exception {
// validator의 실제 메서드가 호출됨
// 특정 메서드만 Stub 가능
doReturn(true).when(validator).isValidCategory(anyString());
// ...
}
}
예외 처리 테스트
@ControllerAdvice와 함께 에러 응답을 테스트할 수 있습니다.
@Test
void 존재하지_않는_상품_조회시_404() throws Exception {
given(productService.findById(999L))
.willThrow(new NotFoundException("상품을 찾을 수 없습니다"));
mockMvc.perform(get("/api/products/{id}", 999L))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("상품을 찾을 수 없습니다"));
}
Security와 함께 테스트
Spring Security가 활성화된 환경에서는 인증 정보를 Mock해야 합니다.
@WebMvcTest(AdminController.class)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ADMIN") // ADMIN 권한으로 테스트
void 관리자_페이지_접근_성공() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
동일한 엔드포인트에 대해 권한별로 다른 응답이 반환되는지 함께 검증해야 보안 규칙이 올바르게 동작한다고 확신할 수 있습니다.
@Test
@WithMockUser(roles = "USER")
void 일반_사용자는_관리자_페이지_접근_불가() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
@Test
void 인증_없이_접근하면_401() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isUnauthorized());
}
}
실무 팁
@WebMvcTest에 컨트롤러를 명시적으로 지정 하세요. 지정하지 않으면 모든 컨트롤러가 로드됩니다@MockBean을 과도하게 사용하면 컨텍스트 캐시가 무효화 되어 테스트가 느려질 수 있습니다- 요청/응답 로그를 보려면
.andDo(print())를 추가하세요 (디버깅에 매우 유용) - 파라미터 검증, 헤더 검증, 에러 응답 형식 등 컨트롤러의 책임에 집중 하세요
주의할 점
1. @WebMvcTest에 컨트롤러를 지정하지 않으면 모든 컨트롤러가 로드된다
@WebMvcTest만 붙이고 대상 컨트롤러를 지정하지 않으면 프로젝트의 모든 @Controller가 스캔됩니다. 각 컨트롤러가 의존하는 서비스마다 @MockBean이 필요해져 설정이 복잡해지고 테스트가 느려집니다. 반드시 @WebMvcTest(ProductController.class)처럼 대상을 명시하세요.
2. Spring Security가 활성화된 상태에서 인증 설정을 빼먹으면 모든 요청이 403/401이 된다
@WebMvcTest는 Security 관련 필터도 로드합니다. 인증이 필요한 엔드포인트를 테스트할 때 @WithMockUser나 Security 설정을 빼먹으면 모든 요청이 인증 실패로 반환됩니다. "컨트롤러 로직은 맞는데 왜 테스트가 실패하지?"라는 상황에 빠지기 쉽습니다.
3. @MockBean으로 만든 Mock의 기본 반환값은 null이므로 NPE에 주의해야 한다
@MockBean으로 등록한 서비스의 메서드는 given()으로 스텁하지 않으면 기본적으로 null을 반환합니다. 컨트롤러에서 서비스 반환값을 바로 사용하면 NullPointerException이 발생하여 500 에러가 나지만, 실제 원인은 Mock 스텁 누락입니다.
정리
@WebMvcTest는 웹 계층만 로드하는 슬라이스 테스트로, 컨트롤러를 빠르게 격리 테스트할 수 있습니다MockMvc로 HTTP 요청을 시뮬레이션하고,jsonPath로 응답을 검증합니다@MockBean으로 서비스 의존성을 Mock하고,@SpyBean으로 부분 Mock을 사용합니다- Security가 활성화된 환경에서는
@WithMockUser로 인증 정보를 설정합니다