본문 바로가기
source-code/FrontEnd

[jest] waitFor을 통한 비동기 함수 테스트

by mattew4483 2024. 6. 23.
728x90
반응형

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/

 

Async Methods | Testing Library

Several utilities are provided for dealing with asynchronous code. These can be

testing-library.com

 

메서드 인터페이스는 아래와 같습니다.

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가 일치하지 않는데도 전체 테스트 결과가 성공으로 나오게 되니, 주의해야 합니다!

728x90
반응형