😀 무려 4시간동안 삽질을 하고도 성공하지 못한 UUID에 대한 TEST코드 삽질 내용을 적어보려고 한다. 진짜 미쳐버리겠다.
나는 아주 간단한 코드에 대해서 테스트 코드를 작성하려고 했다.
import { v4 as uuidv4 } from 'uuid';
//...
async createEmailCode() {
return uuidv4().substring(0, 6);
}
바로 위의 함수이다. uuid모듈에서 v4라는 함수를 꺼내와서 0~(6-1) 인덱스 까지 즉 0,1,2,3,4,5 인덱스의 문자를 가져오는 간단한 함수이다.
이 간단한 함수를 테스트하다가 멘탈이 부셔져 버렸다. 얼핏 보면 굉장히 쉬워보인다.
import * as uuid from 'uuid'
//....
jest.mock('uuid')
//....
it('이메일 인증코드 생성 성공 케이스', async () => {
const spy = jest.spyOn(uuid, 'v4')
const code = await authService.createEmailCode();
expect(spy).toBeCalledTimes(1)
});
jest의 spyOn함수는 어떤 객체에 속한 함수의 호출 여부를 알아낼 때 사용한다.
따라서 나는 uuid의 v4함수가 1번 호출되었는지 확인하고 싶었다. 하지만 바로 실패를 맛보았다.
속성을 재정의 할 수 없다는 에러가 반출되었다. 왜?
v4함수를 step in을 계속하다보면 결과적으로 읽기전용 속성이 나온다.
spyOn함수는 스파이 기능을 구현하기 위해 해당 객체의 메소드를 임시로 대체한다. 따라서 읽기전용인 v4 메소드를 대체할 수가 없다.
따라서 나는 v4함수 자체를 mocking해보고자 아래와 같이 uuid모킹 부분을 변경했다.
import * as uuid from 'uuid'
//....
jest.mock('uuid', () => ({ v4: jest.fn() }))
//....
it('이메일 인증코드 생성 성공 케이스', async () => {
const code = await authService.createEmailCode();
expect(uuid.v4).toBeCalledTimes(1)
});
하지만 제대로 mocking되지 않은 것인지 아래와 같은 에러가 반출되었다.
expect에 들어있는 uuid.v4부분이 mock함수나 spy함수가 아니기 때문에 에러가 발생했다.
jest의 mocking방식은 간단하게 생각하면 3가지다.
const mockUuid = { v4:jest.fn() }
jest.mock('uuid')
uuid.v4 as jest.Mock
1번 방식은 uuid가 provider가 아니기 때문에 모킹을 한다고 해도 전혀다른 가짜 객체가 되어 authService.createEmailCode()
를 실행하여도 동작하지 않는다. 횟수가 항상 0으로 고정된다.
2번 방식이 일반적으로 외부라이브러리 객체에 대한 mocking방식으로 알고 있다. 하지만 위에서 동작하지 않았다.
3번 방식은 적용시켜 보았지만 "게터만 있는 [객체 객체]의 속성 v4를 설정할 수 없습니다"라는 에러가 발생한다.
결국 속성의 재정의 해야하는데 할 수 없다는 맥락에서 1번방식과 비슷한 에러라는 것을 알 수 있다.
내가 알고있는 mocking방식이 모두 실패했다. 따라서 구글을 뒤집어 보았다.
스택 오버플로우에 17개의 답변을 가진 질문의 포스팅을 찾아서 하나하나 살펴보았다. 나와는 달리 직접적인 code의 toEqual값을 테스팅 하려는 목적이었지만, 참고삼아 나도 code값 까지 일치하는지 해봐야겠다는 생각으로 둘러보았다.
첫번째 답변은 나와 비슷한듯 조금 다른 모킹방식이었다. v4를 직접 함수 구현까지 만들어 주었지만, jest.fn()을 사용하지 않았다.
위의 방식을 그대로 실험해 보았지만, 정상동작하지 않았다. 심지어 코드의 일치는 전혀 볼 수 없는 expect의 code는 999ft1이고 내 예상값은 123456이었다. 결국 이건 mocking이 된데 아니라 authService.createEmailCode()
가 동작한 결과가 그대로 result에 담겼고, 결국 mocking되지 않아서 이미 작성된 코드 로직이 그대로 실행된 것이다. 만약 mocking이 잘되었다면, 123456이 나왔겠지? 어떻게 이런 답변이 28개의 좋아요와 채택을 받았을까?
두번째 답변도 22개로 많은 좋아요를 받았는데 mockImplementation을 이용한 방식이었다. 일단 uuid/v4모듈을 따로 받았기 때문에 내 상황과는 달랐다. 내상황에 맞춰서 사용하게 된다면 uuid.v4.mockImplementation()
이 될텐데 이러한 경우 v4에 mockImplementation()속성이 없다는 애러를 반출했다. .mockImplementation()
이나.mockReturnValue()
등의 함수는 mocking이 성공적으로 이루어 져야 쓸 수 있는데, 위와 같은 에러를 뿜는 것으로 봐서 결국 mocking이 정상적으로 이루어 지지 않음을 확인할 수 있었다.
세번째 답변이 딱 나와 생각이 똑같았다. 2020년 12월 24일 크리스마스 이브에 남긴 글인데, 이분은 동작한걸까? 아래의 대댓글 또한 자신의 코드에서 동작했다는데, 왜 나는 spyOn에서 문제가 발생하는 것인지 모르겠다. uuid가 2년 반 사이에 갑자기 readOnly속성으로 변경한걸까?
네번째 답변도 내가 작성한 코드와 다를게 없었다 jest.mock은 describe 밖에서 mocking해 주었기 때문에 이 답변에서 정답을 찾기는 힘들었고, 이와 같이 작성하면 또 spyOn에서 속성 재정의에 대한 문제가 발생한다.
이 외에도 여러 답변이 있었지만, 내게는 참고사항 정도만 될 뿐 대부분이 같은 부분에서 에러를 반출했고, spyOn을 사용하지 않더라도 일단 jest.mock('uuid')의 mocking이 정상적으로 이루어 지지않아서, authService.createEmailCode()
를 실행한 결과와 내가 jest.fn()으로 주는 결과가 같아지지 않고 계속해서 틀리다는 응답만이 돌아왔다.
jest의 공식사이트에서도 아래와 같이 jest.mock('모듈')로 mocking을 진행한다고 친절히 설명하고 있는데 내 코드는 mocking되지 않는다.
이유를 추측해보면 결국 jest.mock('모듈')로 진행하면 내부의 하위 함수들도 모두 mocking되어 대체되는데 결국 이게 또 ReadOnly속성에 의해 mocking되지 않는게 아닐까 싶다.
내가 내린 결론은 현재의 내 지식으로는 uuid의 v4함수는 mocking할 수 없다. 따라서 email코드를 생성하는 로직을 uuid에 의존할게 아니라 Math.random를 사용하여 직접 구현하거나 nanoid와 같은 다른 코드생성 모듈로 변경해야 테스트 코드 작성에 용이할 것 같다.
추가
uuid의 버전을 낮추어서 해결하는 방식이 존재하기는 한다. 하지만 되도록 최신상태의 버전을 사용하는게 좋다고 판단되어 일단은 보류한다.
추가 그냥 해당 UUID의 동작을 직접확인하지 않고, 실제 authService.createEmailCode() 함수가 실행되면, 해당 결과가 6자리가 맞는지? 문자열이 맞는지를 확인함으로서 내부적으로 정상 동작한다는 추측을 할 수 있도록 테스트 코드를 작성하였다.
it('이메일 인증코드 생성 성공 케이스', async () => {
const code = await authService.createEmailCode();
expect(code.length).toBe(6);
expect(typeof code).toBe('string');
});