현재 테스트 프레임워크로 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 함수 내부에서 분기 로직을 처리해야한다는점.위와 같은 이유 떄문에 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으로 어떤 상황이 주어졌을 때 어떤 값을 반환할 것인지 직관적으로 파악할 수 있기 때문에 실제 코드 작성할 때 기존 로직에 대해 몰라도 모킹할 수 있도록 도와준다.
기본 사용법은 공식 github에 정리되어 있으니, 자주 사용되는 when
, verify
, capture
에 대해서만 작성하려고 한다.
when은 특정 상황에서 어떤 반환값 / 행위를 할지 지정할 수 있다.즉, 아래와 같은 상황을 지정할 수 있다.
여기서 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 ErrorthenCall
: 별도의 커스텀 메소드(함수)를 호출thenReturn
: returnthenResolve
: resolve promisethenReject
: rejects promiseverify 는 지정된 인자가 특정 조건 (파라미터 값, 타입, 총 호출된 횟수, 호출 순서 등) 에 맞춰 몇번, 몇번째 순서로 호출되었음을 검증할 수 있다.
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
함수는 모의 객체(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()
를 사용하여 첫 번째 호출에서 캡쳐된 인자를 가져왔습니다.
기존 Jest만 사용해서 mock 할 때 보다 훨씬 직관적이고, 특정 로직에서 반환 값이 달라지는 경우도 쉽게 작성할 수 있어서 좋았다.
다만 Jest mock이 아닌 ts-mockito에 대한 학습도 해야하다보니 기존 mock에 익숙한 사람이라면 금방 배우겠지만, 처음 하는 사람이라면 헷갈릴 수도 있다는 생각이 들었다.
아직 테스트코드를 작성한지 오래 되지 않아 공식문서와 구글링을 통해 찾아가면서 작성하고 있지만, 나중에 손에 익는다면 훨씬 보기 편하고 직관적으로 로직을 파악할 수 있는 테스트코드를 작성하는데 큰 도움이 될 것 같다.