ts-Mockito 도입

Shin·2023년 4월 17일
0

ts-Mockito 라이브러리를 도입하려고 생각한 이유

현재 테스트 프레임워크로 Jest를 선택하고, 도입했지만 Jest의 Mock/Stub 기능의 불편함과 부족함이 느껴져, 이를 보완하기 위한 방법을 찾다가 ts-mockito 를 찾게 되었다.

기존 Jest를 사용하면서 느꼈던 불편함은 아래와 같다.

find_by_pk: jest.fn().mockImplementation(
              async (menu_master_idx: number): Promise<MenuMasterSchema> => {
                if (menu_master_idx === -99) {
                  return null;
                }
                return menu_master_data;
              }
            )
  • 별도의 when 을 지원하지 않아, Jest에서 위와 같이 Stub 함수 내부에서 분기 로직을 처리해야한다는점.
  • IDE의 지원을 받을 수 없는 문자열 베이스
    • Stubbing 메소드 지정을 문자열로 하기 때문에 IDE의 리팩토링, 자동완성 등을 지원받을 수 없다.
    • 특히 직접 메소드를 입력해야하니 오타 문제 등이 존재한다
  • 단순한 클래스 Stub에도 장황한 코드가 필요하다.
  • ts-Mockito는 Mock 라이브러리 이기 떄문에, 다른 테스트 프레임워크를 사용해도 mock 하는 부분은 동일하게 사용할 수 있다.

위와 같은 이유 떄문에 ts-Mockito를 도입하기로 했다.


적용 과정

기존 Service에서 함수를 Mocking 할 때는 아래와 같이 진행했다.

useValue: {
            search_seller_menu_list_v3: jest.fn().mockResolvedValue([menu_master_data]),
            update_menu_name: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            menu_img_url: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            update_menu_origin_price: jest.fn().mockResolvedValue([1, [menu_master_data]]),
            find_by_pk: jest.fn().mockImplementation(
              async (menu_master_idx: number): Promise<MenuMasterSchema> => {
                if (menu_master_idx === -99) {
                  return null;
                }
                return menu_master_data;
              }
            )
          }

그리고 ts-mockito로 작성하면 아래와 같이 된다.

const mock_service = mock(MenuMasterService);
  when(mock_service.find_by_pk(anyNumber())).thenResolve(menu_master_data);
  when(mock_service.find_by_pk(-99)).thenResolve(null);
  when(mock_service.update_menu_name(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.menu_img_url(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.update_menu_origin_price(anything())).thenResolve([1, [menu_master_data]]);
  when(mock_service.search_seller_menu_list_v3(anything())).thenResolve([menu_master_data]);

useValue: instance(mock_service)

기존 Jest로 작성할 때 find_by_pk 는 특정 상황을 만들려면, Stub 함수 내부에서 분기 로직을 처리해야했지만, ts-mockito를 사용하면 위처럼 when으로 어떤 상황이 주어졌을 때 어떤 값을 반환할 것인지 직관적으로 파악할 수 있기 때문에 실제 코드 작성할 때 기존 로직에 대해 몰라도 모킹할 수 있도록 도와준다.


ts-mockito 사용법

기본 사용법은 공식 github에 정리되어 있으니, 자주 사용되는 whenverifycapture 에 대해서만 작성하려고 한다.

when

when은 특정 상황에서 어떤 반환값 / 행위를 할지 지정할 수 있다.즉, 아래와 같은 상황을 지정할 수 있다.

  • A 라는 값이
  • B 라는 메소드로 전달되면
  • C 라는 값이 반환되어야 한다

여기서 A라고 불리는 특정 메소드 인자의 범위는 다음과 같다

  • 1"a"{"menu_name":"치킨"} 등의 고정된 값
  • anyString()anyNumber() 등 문자열, 숫자등의 타입
  • anyOfClass()anyFunction() 등의 클래스, 함수 타입
  • between()objectContaining 등 범위 조건
when(mock_service.find_by_pk(anyNumber())).thenResolve(menu_master_data);
when(mock_service.find_by_pk(-99)).thenResolve(null);
when(mock_service.update_menu_name(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.menu_img_url(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.update_menu_origin_price(anything())).thenResolve([1, [menu_master_data]]);
when(mock_service.search_seller_menu_list_v3(anything())).thenResolve([menu_master_data]);

이때, 결과값은 아래와 같이 지정해줄 수 있다.

  • thenThrow: throw Error
  • thenCall: 별도의 커스텀 메소드(함수)를 호출
  • thenReturn: return
  • thenResolve : resolve promise
  • thenReject: rejects promise

verify

verify 는 지정된 인자가 특정 조건 (파라미터 값, 타입, 총 호출된 횟수, 호출 순서 등) 에 맞춰 몇번, 몇번째 순서로 호출되었음을 검증할 수 있다.

const myServiceMock = mock(MyService);
const myService = instance(myServiceMock);

myService.someMethod(42);

// 검증: someMethod가 인자 42와 함께 호출되었는지 확인합니다.
verify(myServiceMock.someMethod(42)).called();

verify 를 쓸때 주의할 점은 다음과 같다

  • verify 의 인자는 instance() 의 결과가 아닌 mock(MyService) 의 결과가 사용되어야 한다

when 절처럼 instance(mockService) 를 통해 검증해버리면 오류가 발생한다.

verify 자체가 검증문이고, 이때는 mock(MyService) 의 결과를 사용해야함을 주의해야한다.

capture

capture 함수는 모의 객체(mock)의 특정 메서드가 호출될 때 사용된 인자를 캡쳐하여 검사하는 데 사용된다.

다음은 capture의 사용 예제 이다.

const myServiceMock = mock(MyService);
when(myServiceMock.someMethod(anything())).thenReturn('Hello World!');

const myService = instance(myServiceMock);
myService.someMethod(42);

// 검증: someMethod가 호출되었는지 확인합니다.
verify(myServiceMock.someMethod(anything())).called();

// 캡쳐: someMethod의 호출 시 사용된 인자를 캡쳐합니다.
const [capturedArg] = capture(myServiceMock.someMethod).first();
console.log(capturedArg); // 출력: 42

위 예제에서 capture함수를 사용하여 someMethod 호출 시 사용된 인자를 캡쳐하고, first()를 사용하여 첫 번째 호출에서 캡쳐된 인자를 가져왔습니다.


ts-mockito 라이브러리

적용 후기

기존 Jest만 사용해서 mock 할 때 보다 훨씬 직관적이고, 특정 로직에서 반환 값이 달라지는 경우도 쉽게 작성할 수 있어서 좋았다.

다만 Jest mock이 아닌 ts-mockito에 대한 학습도 해야하다보니 기존 mock에 익숙한 사람이라면 금방 배우겠지만, 처음 하는 사람이라면 헷갈릴 수도 있다는 생각이 들었다.

아직 테스트코드를 작성한지 오래 되지 않아 공식문서와 구글링을 통해 찾아가면서 작성하고 있지만, 나중에 손에 익는다면 훨씬 보기 편하고 직관적으로 로직을 파악할 수 있는 테스트코드를 작성하는데 큰 도움이 될 것 같다.

profile
누군가의 선택지가 될 수 있는 사람이 되자

0개의 댓글