Backgounds
jest를 사용해 unit test를 작성하고 있습니다.
테스트 대상 모듈이 올바르게 동작하는지를 확인하기 위해
해당 모듈이 내부적으로 의존하고 있는, 다른 모듈의 반환값을 테스트 구문에서 제어할 필요가 있었습니다.
→ jest의 spyOn API를 사용해 구현할 수 있습니다.
https://jestjs.io/docs/jest-object#jestspyonobject-methodname
// video.ts
export const video = {
play() {
return true;
},
};
// video.test.ts
import { video } from './video.ts'
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
test('plays video', () => {
const spy = jest.spyOn(video, 'play');
const isPlaying = video.play();
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
});
공식 문서 속 예제처럼 사용할 수 있고,
추가적으로 mockImplementation, mockReturnValue 등의 메서드를 사용해
해당 spy의 구현, 반환값을 테스트 구문 내에서 제어해 줄 수도 있습니다.
// video.test.ts
import { video } from './video.ts'
test('plays video', () => {
// play mehtod의 반환값을 mocking
jest.spyOn(video, "play").mockImplementation(() => false);
});
Problems
import로 특정 함수를 불러온 뒤 spyOn을 사용하고자 했습니다.
즉 대상 모듈은 아래와 같았고
// video.ts
export const playVideo = () => true;
테스트 구문에서 해당 함수의 반환값을 제어하고자 했습니다.
// video.test.ts
import * as video from './video.ts'
test('plays video', () => {
jest.spyOn(video, "playVideo").mockReturnValue(false)
});
얼핏 봤을 때는 문제없이 동작할 것 같았지만...
이 경우 Cannot redefine property: playVideo 에러가 발생하고 맙니다!
해당 에러는 상수에 새로운 값을 재할당하려 시도할 때 발생하는데...
playVideo 함수를 mocking하는 것 자체가 불가능한 것일까요?
그렇다면 왜 위 예제에서는 video의 play 메서드를 mocking 할 수 있었던 것일까요?
Caused
spyOn 메서드는 아래와 같이 사용할 수 있습니다.
jest.spyOn(object, methodName)
Creates a mock function similar to jest.fn but also tracks calls to object[methodName]. Returns a Jest mock function.
즉 입력받은 object의 methodName에 해당하는 메서드를 mocking하는데...
이때 mocking 한다 = 객체의 해당 메서드를 변경한다 라고 이해할 수 있겠죠!
그런데 이때, 오류가 발생한 코드를 살펴보면
// video.test.ts
import * as video from './video.ts'
test('plays video', () => {
// import한 module(video) 자체를 변경
jest.spyOn(video, "playVideo").mockReturnValue(false)
// 즉 video.playVideo = () => false 를 시도하는 것과 같은 행위
});
import as 구문을 사용해 모듈 전체를 video에 바인딩했고,
이를 통해 spyOn의 첫 번째 인자로 넘겨줄 수는 있지만 (타입 에러는 발생하지 않지만)
실제 테스트 코드가 동작할 때는
video.playvideo = () => false 와 같이 모듈을 직접 수정하는 행위가 발생할 것이고
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
이는 import 된 변수는 상수처럼 동작한다는(재할당할 수 없다는) Javascript 모듈 시스템 규칙을 어기는 게 되고 맙니다.
→ 발생한 에러 문구인 Cannot redefine property 가 이 상황을 의미하는 것이죠!
// video.test.ts
import { video } from './video.ts'
test('plays video', () => {
// video 모듈이 아닌, play 속성을 수정! => Javascript 모듈 시스템에서도 문제 없음
const spy = jest.spyOn(video, 'play');
const isPlaying = video.play();
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
});
아하! 즉 첫 예제가 정상적으로 동작했던 이유는
import 한 video란 모듈을 직접 재할당한 것이 아니라, 해당 객체의 play method를 변경한 것이기 때문에
Javscript 모듈 시스템에서도 문제가 없었던 것이죠.
Solution
해당 모듈 자체를 mocking
Javscript 모듈 시스템은 import한 모듈에 대한 재할당을 허용하지 않습니다.
→ 그렇다면, import한 모듈이 아닌, 해당 파일 자체를 새롭게 정의하는 것은 아무 문제가 없겠죠!
jest에서는 .mock API를 사용해 특정 모듈 자체를 mocking 할 수 있습니다.
// video.test.ts
import * as video from "./data";
// video.ts가 export하는 module 자체를 Mock
jest.mock("./video.ts");
test('plays video', () => {
// 이 때 import한 video 모듈은, jest.mock 구문에 의해 재정의된 상태
// spyOn을 통해 해당 객체의 playVideo 메서드 반환값 재정의
jest.spyOn(video, "playVideo").mockReturnValue(false);
});
spyOn을 사용하기 전에, 해당 모듈 전체를 새롭게 mocking 해 위 에러를 막을 수 있습니다.
테스트 구문 전체적으로 해당 모듈의 mocking이 필요하다면 해당 구문을 jest.setup에 작성할 수도 있겠죠.
'source-code > FrontEnd' 카테고리의 다른 글
내가 프론트엔드에 클린 아키텍처를 도입한 이유 (0) | 2024.06.12 |
---|---|
[jest] mock.calls를 통한 mock 함수 실행 테스트 개선하기 (0) | 2024.06.04 |
[chrome extension] 에러 핸들링 (0) | 2024.02.02 |
[chrome extension] side panel 사용하기 (0) | 2024.02.01 |
next 14 Amplify 배포 시 crypto 에러 해결 (1) | 2024.01.24 |