컨트롤러를 테스트할 때 서비스, 리포지토리, DB까지 전부 띄워야 할까요?

컨트롤러의 책임은 "요청을 받아 서비스에 위임하고, 응답을 반환하는 것"입니다. 서비스 로직이나 DB는 컨트롤러 테스트의 관심사가 아닙니다. @WebMvcTest는 웹 계층만 잘라서 빠르게 테스트할 수 있게 해주는 슬라이스 테스트입니다.

@WebMvcTest란

@WebMvcTest는 Spring Boot의 슬라이스 테스트 어노테이션으로, 웹 계층 관련 빈만 로드 합니다.

로드되는 빈:

  • @Controller, @RestController
  • @ControllerAdvice, @RestControllerAdvice
  • @JsonComponent
  • Filter, WebMvcConfigurer
  • HandlerMethodArgumentResolver

로드되지 않는 빈:

  • @Service, @Repository, @Component
  • 데이터 소스, JPA 관련 설정
JAVA
@WebMvcTest(ProductController.class)  // 특정 컨트롤러만 지정
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean  // Service를 Mock으로 대체
    private ProductService productService;

    // 테스트 코드
}

MockMvc 기본 사용법

MockMvc는 서버를 띄우지 않고 DispatcherServlet을 직접 호출하여 HTTP 요청을 시뮬레이션합니다.

GET 요청 테스트

JAVA
@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 요청 테스트

JAVA
@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("새 상품"));
}

유효성 검증 실패 테스트

JAVA
@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 객체로 교체합니다.

JAVA
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @MockBean
    private AuthService authService;  // Security 관련 서비스도 Mock

@MockBean으로 서비스 의존성을 Mock한 뒤 given()으로 반환값을 설정하고 요청을 검증합니다.

JAVA
    @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 TestMockito
컨텍스트Spring ApplicationContext에 등록Mockito 단독
용도슬라이스/통합 테스트순수 단위 테스트
성능컨텍스트 캐시 무효화 가능빠름

@SpyBean — 부분 Mock

실제 빈의 메서드를 호출하되, 특정 메서드만 Stub하고 싶을 때 사용합니다.

JAVA
@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @SpyBean
    private ProductValidator validator;  // 실제 로직 수행, 일부만 Stub

    @Test
    void 검증_로직을_거치는_등록_테스트() throws Exception {
        // validator의 실제 메서드가 호출됨
        // 특정 메서드만 Stub 가능
        doReturn(true).when(validator).isValidCategory(anyString());

        // ...
    }
}

예외 처리 테스트

@ControllerAdvice와 함께 에러 응답을 테스트할 수 있습니다.

JAVA
@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해야 합니다.

JAVA
@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());
    }

동일한 엔드포인트에 대해 권한별로 다른 응답이 반환되는지 함께 검증해야 보안 규칙이 올바르게 동작한다고 확신할 수 있습니다.

JAVA
    @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로 인증 정보를 설정합니다
댓글 로딩 중...