mock을 왜 사용하고 있었지?

yboy·2022년 8월 29일
0

학습로그

목록 보기
17/30
post-thumbnail

학습 동기

현재 프로젝트의 controller를 mockito와 MockMvc를 이용해 테스트하고 있다. 우아한테크코스 level2 장바구니 미션의 기본으로 주어진 controllerTest 코드에서 mockito와 MockMvc를 사용하고 있었기 때문에 무지성으로 사용하고 있었던 것….. 그럼 알아보자… mock이란 무엇이고 우리는 왜 controllerTest를 mock으로 하고 있는 걸까 그리고 여기서 가질 수 있는 이점은 무엇일까?

학습 내용

우선 mockito와 MockMvc가 무엇인지 알아보자.

mockito

  • 단위 테스트를 위한 Java Mocking Framework
  • JUnit에서 가짜 객체인 Mock객체를 생성해주고 관리하고 검증할 수 있도록 지원해주는 Framework
  • 구현체가 아직 없는 경우나 구현체가 있더라도 특정 단위만 테스트하고 싶을 경우 사용할 수 있도록 적절한 환경을 제공해준다.
  • 스프링부트 2.2+ 프로젝트 생성시 spring-boot-start-test에서 자동으로 Mokito를 추가해준다.(우리는 2.7.1을 사용 중)
  • 스프링 부트를 쓰지 않는다면, 의존성을 직접 추가해야 한다.(우리는 스프링 부트를 이용하고 있으므로 해당되지 않음)

mock(객체)이란?

  • Mock 객체란 개발한 프로그램을 테스트할 때 테스트를 수행할 모듈과 연결되는 외부의 다른 모듈을 흉내 내는 가짜 모듈을 생성하여 테스트의 효율성을 높이는 데 사용하는 객체

프로젝트에서 mockito 사용 용도

예시1)

@MockBean
protected ScheduleService scheduleService;

예시1과 같이 @Mockbean 을 이용해 실제 spring에서 관리하는 bean객체를 주입받는 것이 아닌 이를 흉내낸 mock 객체를 주입받아 사용하고 있다.

예시2)

 given(jwtTokenProvider.validateToken(token))
                .willReturn(true);

예시2와 같이 given().willReturn() 을 사용해 특정상황을 Stubbing(행동 정의) 해주고 있다.

MockMvc

  • MockMvc는 웹 어플리케이션을 어플리케이션 서버에 배포하지 않고 테스트용 MVC환경을 만들어 요청 및 전송, 응답 기능을 제공해주는 유틸리티 클래스

쉽게 말해 보자면,

만약 컨트롤러 테스트를 하고 싶을 때 실제 서버에 구현한 애플리케이션을 올리지 않고 테스트용으로 시뮬레이션하여 MVC가 되도록 도와주는 클래스

프로젝트에서 MockMvc 사용 용도

예시)

protected MockHttpServletRequestBuilder get(String url) {
        return MockMvcRequestBuilders.get(url)
                .contentType(MediaType.APPLICATION_JSON)
                .characterEncoding("UTF-8");
    }
mockMvc.perform(get("/api/v2/crews/me/reservations")
                .header("Authorization", "Bearer " + token))
                .andDo(print())
                .andExpect(status().isOk());

예시와 같이 perform()을 이용하여 설정한 MockMvc를 실행하고 andExpect()로 테스트를 진행하고 있다.

perform()

  • MockMvc가 제공하는 메서드로, 브라우저에서 서버에 URL 요청을 하듯 컨트롤러를 실행시킬 수 있다.
  • perform() 메소드는 RequestBuilder 객체를 인자로 받고, 이는 MockMvcRequestBuilders의 정적 메소드를 이용해서 생성한다.

andExpect()

  • perform() 메소드를 이용하여 요청을 전송한 후, 응답 결과를 검증할 수 있는 메소드

예시처럼 andExpect()로 status를 확인하는 것 이외에도

mockMvc.perform(get("/api/v2/crews/1/reservations")
                .header("Authorization", "Bearer " + token))
                .andDo(print())
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("message").value("존재하지 않는 크루입니다."));

위와 같이 jsonpath로 예외상황의 json응답 값의 message 값도 확인하고 있다.

이제 예외 상황에 대한 mocking 처리를 어떻게 하고 있는 지 보자.

예시)

@DisplayName("코치가 크루의 면담 목록 조회에 실패한다. -  존재하지 않는 크루 아이디")
@Test
void coachFindCrewReservations_notFoundCrewId() throws Exception {
        String token = "나 코치다";
        코치의_토큰을_검증한다(token);

        doThrow(new NotFoundCrewException()).when(reservationService)
                .findCrewHistoryByCoach(anyLong());

        mockMvc.perform(get("/api/v2/crews/1/reservations")
                .header("Authorization", "Bearer " + token))
                .andDo(print())
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("message").value("존재하지 않는 크루입니다."));
    }

위 예시는 api의 request url의 path valiable값으로 전달되는 id값이 db에 존재하지 않을 경우에 대한 에러케이스 테스트이다.

doThrow(new NotFoundCrewException()).when(reservationService) .findCrewHistoryByCoach(anyLong());

어떤 경우에 대한 예외를 고의로 발생시킬 때 위의 doThrow().when() 으로 간편하게 예외를 유발시킬 수 있다.

그래서 Controller Test를 왜 mock으로 하는 건데?

테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스을 단축하기 위해

우리가 controller 테스트를 하는 목적은 HTTP status를 확인하기 위함이다.(추후에 Restdocs를 이용한 문서화에도 이용할 예정) 지금은 RestAssured를 사용하는 인수 테스트를 Restdocs를 이용한 문서화로 이용하고 있지만 RestAssured를 사용하면 given에 대한 테스트 코드가 방대해지기 때문에 예외 사항에 대한 테스트를 하기 번거롭다. 따라서 MockMvc를 이용한 컨트롤러 테스트를 Restdocs를 이용한 문서화에 이용하는 것이 적절할 것으로 보인다.

controller 테스트는 그저 간단하게 client에게 HTTP Status를 잘 응답하고 있는 지를 테스트하기 위함인데 RestAssured와 @SpringbootTest를 이용하는 것은 올바르지 않다고 생각한다. service의 내부 메서드를 알아야 하거나 테스트를 위해 db에 데이터를 save하거나 로그인을 통해 토큰을 받아오는 번거로운 작업을 mocking 함으로써 테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스를 단축하는 것이 맞다는 판단 하에 controller를 mockito를 이용해 테스트하고 있다.

🪃트러블 슈팅

1. doThrow().when()을 이용해 예외를 유발시킬 때 발생한 에러

doThrow(NotFoundCrewException.class).when(reservationService)
                .findCrewHistoryByCoach(anyLong());

위와 같이 NotFoundCrewException.class 로 하면 예외 상황에서 HTTP 상태 코드는 잘 오지만 응답 Body값의 칼럼 정보인 message가 null로 반환된다.

아래와 같이

doThrow(new NotFoundCrewException()).when(reservationService)
                .findCrewHistoryByCoach(anyLong());

new NotFoundCrewException() 로 하면 message 값 확인이 가능하다.

2. 사용 메서드의 실행 흐름에 검증하는 로직이 있다면 이를 mocking처리 해줘야 한다.

api의 header에 jwt token을 받도록 api가 수정되었다. 아래와 같이 token에 임의 값을 넣고 테스트를 돌리면 401에러가 발생했다.

@Test
void coachFindCrewReservations_invalidCrewId() throws Exception {
        String token = "token";

        mockMvc.perform(get("/api/v2/crews/a/reservations")
                .header("Authorization", "Bearer " + token))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }    

테스트가 실패한 이유는 메서드의 실행 흐름에 검증하는 로직이 있었기 때문이였는데 mock 테스트를 할 때는 실행 흐름에 검증하는 로직이 있다면 이것에 대해 default로 에러를 내던가 false를 반환한다.

따라서,

@Test
void coachFindCrewReservations_invalidCrewId() throws Exception {
        String token = "token";

				given(jwtTokenProvider.validateToken(token))
                .willReturn(true);

        mockMvc.perform(get("/api/v2/crews/a/reservations")
                .header("Authorization", "Bearer " + token))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }   

위의 코드와 같이 given().willReturn() 으로 이를 처리해야 다름 실행 순서로 넘어가 우리가 유도하는 흐름대로 테스트가 동작하게 된다.
여기서 궁금증이 생겼다. given()이 Mockito를 import하는 것이 아니라 아니라 BDDMockito import하고 있었다는 것.... 그럼 BDDMockito는 무엇일까??

BDDMockito

우선 BDD에 대해 알아보자.

Behavior-Driven Development의 약자로 행위 주도 개발을 말한다. 테스트 대상의 상태의 변화를 테스트하는 것이고, 시나리오를 기반으로 테스트하는 패턴을 권장한다.

여기서 권장하는 기본 패턴은 Given, When, Then 구조를 가진다.

사실 Mockito와 BDDMockito가 엄청난 차이점이 존재하고 있는 것은 아니였다. BDDMockito는 Mockito와 기능이 같으나, 사용자의 시나리오에 맞게 테스트코드가 읽힐 수 있도록 도와준다는 차이점(이것은 큰 장점이라고 생각!)이 있었던 것이 었다.

@Test
void test() {
	//given
    
    //when
    
    //then

}

가독성 측면에서 when()보다 given()을 사용하는 것이 더 낫다고 생각한다. 무지성으로 BDDMockito를 사용하고 있었지만 결론적으로 나의 기호에 더 맞는 framework를 사용하고 있었던 것...ㅎㅎ

참고: Mockito를 사용한다면 when().thenReturn()으로 Stubbing을 할 수 있다.

마무리

프로젝트에서 무지성으로 사용하고 있었던 mock 테스트에 대해 알아볼 수 있는 시간이여서 너무 유익했다. 내가 당연스레 사용하고 있는 건데 이걸 왜 사용하고 있는 거지?라는 의문이 들 때 학습을 하는 것 만큼 재밌는 일은 없는 것 같다. 앞으로도 홧팅!!

0개의 댓글