Backgrounds
비동기 함수에 대한 단위 테스트를 작성해야 할 때가 있습니다.
function someTask(): Promise<boolean> {
return new Promise((resolve) => resolve(true));
}
it('비동기 함수 테스트', () => {
const result = someTask();
expect(result).toBe(true);
});
위와 같이 테스트 코드를 작성하면... 당연히 실패합니다!
someTask는 Promise를 반환하는 비동기 함수이므로
expect 구문이 실행되는 시점에는, 해당 값은 Promise<boolean>이기 때문이죠.
function someTask(): Promise<boolean> {
return new Promise((resolve) => resolve(true));
}
it('비동기 함수 테스트', async () => {
const result = await someTask();
expect(result).toBe(true);
});
async-await 구문을 통해 해당 Promise가 resolve된 이후 expect를 실행하면, 원하는 결과를 얻을 수 있습니다.
Problems
그런데 만약 단위 테스트 대상 함수가 Promise를 반환하지 않는다면 어떻게 해야 할까요?
function someTask(callback: () => void): void {
new Promise((resolve) => resolve(true)).then(() => {
console.log('해당 scope내 callback 함수 실행 여부를 테스트하고 싶습니다.');
callback();
});
}
it('비동기 함수 테스트', () => {
const callbackMockFn = jest.fn();
someTask(callbackMockFn);
expect(callbackMockFn).toHaveBeenCalled();
});
당연하게도, 위 테스트 코드 역시 실패합니다.
이유는 간단하죠.
someTask 함수 내 Promise는 코드가 실행되었을 때 callback queue에 들어갈 테고
이로 인해 해당 Promise가 resolve 되기 전에 expect문이 실행되기 때문입니다.
(즉 expect문 실행 시점에는, 아직 callbackMockFn은 실행되지 않은 것)
또한 someTask 함수 자체는 Promise를 반환하지 않기 때문에, 해당 비동기 작업의 완료 시점을 외부에서 알 방법도 없습니다.
Solution
timeout으로 강제 대기
가장 단순무식한 방법입니다.
function someTask(callback: () => void): void {
new Promise((resolve) => resolve(true)).then(() => {
console.log('해당 scope내 callback 함수 실행 여부를 테스트하고 싶습니다.');
callback();
});
}
it('비동기 함수 테스트', () => {
const callbackMockFn = jest.fn();
someTask(callbackMockFn);
setTimeout(() => expect(callbackMockFn).toHaveBeenCalled(), 0);
});
expect문을 setTimeout의 callback 함수에서 호출했기 때문에
someTask의 callback과 expect문 모두, callback queue를 거친 후 call stack에서 실행되겠죠!
→ 위 상황에서 someTask의 Promise는 즉시 resolve 되므로, timeout내 expect문 보다 먼저 실행돼, 테스트가 성공할 수 있습니다.
하지만 안 하는 게 나을 정도로 사용하지 않는 게 나은 방법입니다.
왜? 해당 비동기 작업이 언제 끝날지 알 수 없으며, 이로 인해 거짓 양성 결과가 증가할 게 뻔하기 때문이죠!
testing-library의 waitFor 사용하기
testing-library의 waitFor 메서드를 사용할 수 있습니다!
https://testing-library.com/docs/dom-testing-library/api-async/
메서드 인터페이스는 아래와 같습니다.
function waitFor<T>(
callback: () => T | Promise<T>,
options?: {
container?: HTMLElement
timeout?: number
interval?: number
onTimeout?: (error: Error) => Error
mutationObserverOptions?: MutationObserverInit
},
): Promise<T>
waitFor은 콜백 함수 내 작성된 조건이 충족할 때까지 기다리는 동작을 수행하며,
기본 timeout은 1000ms, 기본 폴링 간격은 50ms입니다.
일반적으로 프론트엔드 테스트 환경에서 특정 상태 변화를 테스트할 때 사용하는데...
지금처럼 비동기 동작에 대한 단위 테스트를 작성할 때도 유용하게 적용할 수 있겠죠!
import { waitFor } from '@testing-library/react';
function someTask(callback: () => void): void {
new Promise((resolve) => resolve(true)).then(() => {
console.log('해당 scope내 callback 함수 실행 여부를 테스트하고 싶습니다.');
callback();
});
}
it('비동기 함수 테스트', async () => {
const callbackMockFn = jest.fn();
someTask(callbackMockFn);
await waitFor(() => {
expect(callbackMockFn).toHaveBeenCalled();
});
});
이때 잊지 말아야 하는 사실은
→ waitFor 자체도 비동기 작업이기 때문에, 정상적인 테스트 실행을 위해서는 해당 작업을 대기해줘야 한다는 것!
import { waitFor } from '@testing-library/react';
function someTask(callback: () => void): void {
new Promise((resolve) => resolve(true)).then(() => {
console.log('해당 scope내 callback 함수 실행 여부를 테스트하고 싶습니다.');
callback();
});
}
it('비동기 함수 테스트', async () => {
const callbackMockFn = jest.fn();
someTask(callbackMockFn);
// expect문을 평가하기도 전에 테스트가 종료됩니다
// 이로 인해 테스트가 실패해야하는데도, 성공 처리 되는 모습
waitFor(() => {
expect(callbackMockFn).not.toHaveBeenCalled();
});
});
즉 위와 같이 waitFor 함수를 실행만 할 경우
matcher가 일치하지 않는데도 전체 테스트 결과가 성공으로 나오게 되니, 주의해야 합니다!
'source-code > FrontEnd' 카테고리의 다른 글
[chrome extension] dynamic import시 Cannot find module 에러 해결하기 (0) | 2024.07.26 |
---|---|
WebComponent에서 styled-components 사용하기 (0) | 2024.07.10 |
[jest] test.each를 통해 여러 입력값 테스트 간결하게 구현하기 (0) | 2024.06.13 |
내가 프론트엔드에 클린 아키텍처를 도입한 이유 (0) | 2024.06.12 |
[jest] mock.calls를 통한 mock 함수 실행 테스트 개선하기 (0) | 2024.06.04 |