fragile and resilient

Spring

[Test] 컨트롤러 테스트해야 할까? (feat. 테스트에 대한 혼동)

Green Lawn 2022. 5. 25. 17:01

문제 상황

테스트 코드를 작성하는 과정에서 테스트 종류에 대한 혼동인수 테스트, 컨트롤러 테스트에 대한 차이에 대한 의문이 들었습니다.
해당 고민에 대한 개인적인 생각을 정리해보고자 합니다.

우선 테스트 종류에 대해 알아봅시다.
테스트 종류

  • 단위 테스트
    • 구현한 부분의 단위를 검증 (하나의 클래스 또는 하나의 메서드)
  • 통합 테스트
    • 각 단위들이 유기적으로 잘 동작되는지 검증
  • E2E 테스트
    • 요구사항을 만족하는지 검증

인수 테스트 (Acceptance Test)

  • API 접점을 검증하는 E2E 테스트
  • 사용자 스토리를 검증하는 기능 테스트
  • 타겟은 프론트엔드 개발자 혹은 API 활용하는 사람
  • API의 Request와 Response 정보 이외의 내부 정보를 최대한 가리는 블랙 박스 형식의 테스트


각각의 테스트는 위의 특징을 가지고 있습니다.
여기서 고민했던 점들에 대해 코드를 바탕으로 설명해보겠습니다.

public class PathAcceptanceTest extends AcceptanceTest {

    @Test
    @DisplayName("최단 경로 조회")
    void showShortestPath() {

        /*
         * Scenario: 최단 경로 조회
         *   Given 지하철 역들이 등록되어 있다.
         *   And 지하철 노선이 등록되어 있다.
         *   And 지하철 구간이 등록되어 있다.
         *   When 출발 역과 도착역의 최단 경로를 요청한다.
         *   Then 최단 경로의 역과 거리, 요금을 응답받는다.
         * */

        //given
        insert(new StationRequest("교대역"), "/stations", 201);
        insert(new StationRequest("강남역"), "/stations", 201);
        insert(new StationRequest("역삼역"), "/stations", 201);
        insert(new StationRequest("선릉역"), "/stations", 201);

        LineRequest lineRequest = new LineRequest("2호선", "green", 1L, 2L, 10, 0);
        long id = insert(lineRequest, "/lines", 201).extract().jsonPath().getLong("id");

        insert(new SectionRequest(2L, 3L, 40), "/lines/" + id + "/sections", 201);

        //when & then
        select("/paths?source=1&target=3&age=15", 200)
                .body("stations.size()", is(3))
                .body("distance", is(50))
                .body("fare", is(2050));
    }
}
        
public static ValidatableResponse select(String path,intstatusCode) {
return RestAssured.given().log().all()
            .when()
            .get(path)
            .then().log().all()
            .statusCode(statusCode);
}

위의 코드는 시나리오를 작성하여 지하철역 간의 최단 경로를 확인하는 인수 테스트인데요.
코드를 보면, RestAssured를 사용하여 실제 요청을 보내서 전체적인 로직을 테스트하고 있습니다.

여기서 한 가지 의문이 들었습니다.

컨트롤러 테스트와 인수 테스트의 차이점은 무엇일까?

위의 인수 테스트에서는 요청을 보내 End-to-End로 전체적인 비즈니스 로직을 테스트하고 있습니다.
컨트롤러는 사용자 요청을 받아 모델에 데이터를 넘기고, 모델에서 비즈니스 로직을 처리하고, 해당 결과를 응답하는 일종의 매개체 역할로 볼 수 있습니다.
그렇다면 컨트롤러 테스트에서도 결국 API 접점을 검증하는 E2E 테스트를 하게 되는 것이 아닌가?라는 생각을 했어요.
위의 이유 때문에 컨트롤러 테스트의 필요성을 느끼지 못했습니다.

그러던 중 MockMvc에 대해 학습하였는데요.

MockMvc

  • MockMvc를 통해 Spring MVC 애플리케이션을 테스트할 수 있다.
  • 실행중인 서버없이 Spring MVC 컨트롤러 테스트를 할 수 있도록 한다.
  • 전체적인 MVC 요청 처리를 실행 중인 서버 대신 모의 객체를 통해 요청 및 응답을 테스트할 수 있다.
  • 컨트롤러를 인스턴스화하고 종속성을 주입하여 Spring MVC에 대한 단위 테스트를 작성할 수 있다.

MockMvc는 위의 특징을 가지고 있습니다.
MockMvc로 구현한 컨트롤러 테스트를 한번 살펴보겠습니다.

@WebMvcTest(controllers = {PathController.class})
class PathControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PathService pathService;

    @Test
    @DisplayName("지하철 역 목록 조회")
    void findStation() throws Exception {
        PathResponse pathResponse = new PathResponse(List.of(new Station("강남역")), 10, 10);

        given(pathService.findShortestPath(new PathRequest(1L, 2L, 10L))).willReturn(pathResponse);

        mockMvc.perform(get("/paths")
                        .param("source", String.valueOf(1L))
                        .param("target", String.valueOf(2L))
                        .param("age", String.valueOf(10L)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("stations[0].id").value(0L))
                .andExpect(jsonPath("stations[0].name").value("강남역"))
                .andExpect(jsonPath("fare").value(10))
                .andExpect(jsonPath("distance").value(10))
                .andDo(print());
    }
}

@WebMvcTest

  • MockMvc에 관한 설정을 자동으로 수행해준다.
  • 해당 어노테이션을 사용하면 테스트와 관련된 configuration만 적용한다. (@Controller, @ControllerAdvice, @JsonComponent 등) 하지만 @Component, @Service, @Repository는 적용하지 않는다.

일반적으로 @WebMvcTest는 @MockBean(기존에 사용하던 Spring Bean 대신 Mock Bean 주입) 또는 @Import와 함께 사용되어, @Controller 빈에 필요한 것들을 생성한다.

 

@MockBean

@MockBean으로 선언된 빈을 주입받는다면, ApplicationContext에 의해서 Mock 객체를 주입해준다. 새롭게 @MockBean을 선언하면 Mock 객체를 빈으로써 등록하지만, 만일 @MockBean으로 선언한 객체와 같은 이름과 타입으로 이미 빈으로 등록되어있다면 해당 빈은 선언한 Mock 빈으로 대체된다.

정리

MockMvc를 사용하면 컨트롤러단만 슬라이스하여 테스트를 할 수 있게 됩니다.
따라서 컨트롤러에서는 요청과 응답만 테스트할 수 있게 됩니다.
이렇게 되면 요구 사항의 전체적인 테스트는 인수 테스트에서 수행하고, 비즈니스 로직은 도메인과 서비스, 요청과 응답은 컨트롤러에서 테스트하면 각자 테스트해야 하는 역할을 잘 수행하고 있다고 생각했습니다.

  • 결론적으로 (인수 테스트와 목적은 서로 다르지만) MockMvc를 사용하면 컨트롤러 테스트를 하는 의미가 있다고 생각했습니다.


더불어 테스트 코드를 작성하면서 자연스럽게 드는 의문이 있는데요.
해당 테스트는 단위 테스트인가? 통합 테스트인가? E2E 테스트인가?라는 생각이 들 때가 있습니다.
해당 의문에 대한 해답은 해당 테스트의 목적에 대해 다시 생각해보자는 것입니다.
또, 모든 테스트가 명확하게 종류로 나뉘지는 않는다고 생각했습니다.


References

브리, 브라운의 명강의 ✨
https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#spring-mvc-test-framework
https://meetup.toast.com/posts/124
https://tecoble.techcourse.co.kr/post/2020-08-19-rest-assured-vs-mock-mvc/
https://jojoldu.tistory.com/226