Mock으로 테스트하기

아현·2023년 10월 12일
0

spring test code

목록 보기
4/4
post-thumbnail

외부 시스템을 사용하거나 의존하는 클래스에 대해 단위 테스트를 완벽해서 통합 테스트를 하지 않아도 되는 경우 가짜 객체를 사용해서 테스트 환경을 편리하게 만들 수 있다.

Mock의 의미와 사용하는 이유

Mock은 흉내 내며 놀리다, 흉내내다 or 가짜의 의미로 spring 에서는 가짜 객체라고 보면 된다.

테스트하면서 의존성 주입을 피하기위해 또는 얽혀있는 의존관계에서 나오는 에러를 방지한 단위 테스트를 위해 사용한다.

(테스트를 하기위해서 준비해야하는 것들이 많을 때 사용한다.)

Test Double이란?

의존성이나 외부 시스템으로 인해 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 가짜 객체이다.

영화 촬영 시 위험한 역할을 대신하는 스턴트 더블에서 비롯되었다.

마틴 파울러가 작성한 Mock관련 정보: https://martinfowler.com/articles/mocksArentStubs.html

Mock은 Stub이 아니다!
Stub은 상태 검증(State Verification)이고, Mock은 행위 검증(Behavior Verification)이다.

Test Double 5가지 종류

  1. Dummy - 아무것도 하지 않는 깡통 객체
  2. Fake - 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체
    프로덕션 코드에서 사용하는 repository대신 Map을 사용하여 기능을 수행하는 등..
  3. Stub - 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 않음.
  4. Spy - Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체.
    일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다.(개발자가 원하는 메서드의 행위를 지정, 다른 메서드는 실제 객체처럼 동작)
  5. Mock - 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체

Test Double에서 Mock을 가장 많이 사용하기 때문에 Mock을 어떻게 사용하는지에 대해서만 정리한다.

Mock 사용법

Mock(가짜객체은 @MockBean 어노테이션을 사용해서 만들어야하는데 @MockBean 어노테이션에 필요한 설정을 2가지 방법으로 할 수 있다.

@SpringBootTest 어노테이션을 사용하는 방법

@SpringBootTest
class productControllerTest {

    @MockBean
    private ProductService productService;
		...
}

두 번째는 @WebMvcTest 어노테이션을 사용하는 방법이다.

@WebMvcTest(controllers = productController.class)
class productControllerTest {
    @MockBean
    private ProductService productService;
		...
}

@SpringBootTest 어노테이션은 모든 빈을 로드하고 @WebMvcTest는 웹 계층의 컴포넌트만 로드하기때문에 빠르지만 의존성이 있는 경우 개발자가 세팅해야한다.

설정 방법을 선택했으면 @MockBean 어노테이션을 사용하여 가짜 객체를 만들 수 있다.

@MockBean
private ProductService productService;

가짜 객체를 사용해서 테스트할 프로덕션 코드를 보자.

@GetMapping("/api/v1/products/selling")
public ApiResponse<List<ProductResponse>> getSellingProducts() {
    return ApiResponse.ok(productService.getSellingProducts());
}

GET요청으로 판매상품을 조회하는 컨트롤러 계층 코드이다.

만약 given에 아무조건을 주지 않고 테스트를 돌려보면 통과된다.

@DisplayName("판매상품을 조회한다.")
@Test
void getSellingProducts() throws Exception {
    // given
		
    // when // then
    mockMvc.perform(
                    MockMvcRequestBuilders.get("/api/v1/products/selling")
            )
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("OK"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("OK"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.data").isEmpty());
}

Mocking된 Bean에 아무런 행위를 주지 않으면 해당 메서드의 기본값을 반환한다.

해당 테스트에는 모킹된 서비스 계층의 getSellingProducts() 메서드를 사용하는데 이 메서드에 행위를 지정해주지 않아 Mockito의 기본값인 null값이 출력된다.

응답 결과

MockHttpServletResponse:
       Status = 200
Error message = null
      Headers = [Content-Type:"application/json"]
 Content type = application/json
         Body = {"code":200,"status":"OK","message":"OK","data":[]}
Forwarded URL = null
Redirected URL = null
      Cookies = []

응답 결과를 보면 data에 null들어가 있는 것을 볼 수 있다.

만약 메서드가 primitive type을 반환하면 0 이나 false를 반환하고 String을 포함한 객체면 null을 반환한다.

Stubbing

Mockito의 기본 값을 알아봤으니 이제 모킹된 메서드의 행위를 지정해보자.

나는 모킹된 productService 클래스의 getSellingProducts() 메서드가 판매상품 리스트를 반환하고 싶다.

모킹된 메서드가 판매상품 리스트를 반환하기 원하기 때문에 먼저 상품을 두개를 만들어 리스트로 만들고 result라 선언했다.

이제 행위를 지정해야 한다.

when() 안에는 행위를 지정하고 싶은 메서드를 입력하고, thenReturn()에는 반환하고 싶은 결과값을 넣는다.

나는 productService.getSellingProducts() 메서드가 설정한 result 를 반환하고 싶기 때문에

Mockito.when(productService.getSellingProducts()).thenReturn(result);

이렇게 코드를 작성했다.

@DisplayName("판매상품을 조회한다.")
    @Test
    void getSellingProducts() throws Exception {
       // given
        ProductResponse productResponse1 = ProductResponse
                .builder()
                .id(1L)
                .productNumber("001")
                .type(HANDMADE)
                .sellingStatus(SELLING)
                .name("아이스 아메리카노")
                .price(4000)
                .build();

        ProductResponse productResponse2 = ProductResponse
                .builder()
                .id(2L)
                .productNumber("002")
                .type(HANDMADE)
                .sellingStatus(SELLING)
                .name("따뜻한 아이스 아메리카노")
                .price(5000)
                .build();
        List<ProductResponse> result = List.of(productResponse1, productResponse2);
				// Stubbing
        Mockito.when(productService.getSellingProducts()).thenReturn(result);

        // when then
				...
    }

이렇게 Mock 객체에 행위를 지정하는 것을 stubbing 이라고 한다.

// when // then
mockMvc.perform(
                MockMvcRequestBuilders.get("/api/v1/products/selling")
        )
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("OK"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("OK"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data").isArray())
        .andExpect(MockMvcResultMatchers.jsonPath("$.data.length()").value(2))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].productNumber").value("001"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data[0].name").value("아이스 아메리카노"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data[1].productNumber").value("002"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data[1].name").value("따뜻한 아이스 아메리카노"));

then 절은 MockMvc를 사용하여 테스트 한다.

응답이 리스트이기때문에 “$.data[index]”로 응답값에 접근해서 예상 값을 작성한다.

BDDMockito

@DisplayName("판매상품을 조회한다.")
    @Test
    void getSellingProducts() throws Exception {
       // given
				...
        List<ProductResponse> result = List.of(productResponse1, productResponse2);
				// Stubbing
        Mockito.when(productService.getSellingProducts()).thenReturn(result);

        // when // then
				...
    }

위에 stubbing 한 코드를 보면 조금 이상한 부분이 있다.

바로 given 절에서 Mockito.when() 을 사용하고 있는 것이다!

Mocktio를 사용해도 BDD(행위주도개발)의 기본 스타일을 지키고 싶은 개발자들이 만든게 BDDMockito이다.

BDDMockito의 구현 코드를 보면 Mocktio를 상속받고 있는데 실제 기능상 별 차이는 없다.

다만 BDD 스타일인 given, when, then에 맞게 메서드 이름만 달라졌다.

public class BDDMockito extends Mockito {
		...
}

위에서 stubbing한 코드를 BDDMockito로 바꿔 보면

@DisplayName("판매상품을 조회한다.")
    @Test
    void getSellingProducts() throws Exception {
       // given
				...
        List<ProductResponse> result = List.of(productResponse1, productResponse2);
				// Stubbing
        // Mockito.when(productService.getSellingProducts()).thenReturn(result);
				BDDMockito.given(productService.getSellingProducts()).willReturn(result);
        // when // then
				...
    }

given(), willReturn()처럼 메서드 이름만 달라졌을 뿐 구조는 같은걸 볼 수 있다.

기존 테스트 코드를 BDD 스타일로 개발하고 있다면 Mockito도 BDDMockito를 사용해서 코드 가독성을 높여보자!


이 포스트는 Practical Testing: 실용적인 테스트 가이드 강의를 참고하여 작성했습니다.

profile
개발 정말 잘하고 싶다

0개의 댓글