React 비동기 작업을 위한 테스트 하는법 (feat. React testing library)
아래 포스트를 읽기 전 Jest와 React Testing Library에 대해 이해가 없다면 이전 글을 보고 오시는 것을 추천드립니다.
비동기 작업을 고려하여 테스트 코드를 작성해야 하는 이유
개발을 하다 보면 비동기 작업을 하는 경우가 많습니다. Click, 렌더링 등에 이벤트가 발생했을 때 특정 로직을 수행하도록 코드를 작성한 경험이 많을 겁니다. 또 로직을 즉시 실행할 수도 있지만 의도적으로 특정 시간 이후 실행되도록 한 경험도 있을 것이라 생각합니다.
간단한 예로 렌더링 된 후 2초가 지났을 때 화면에 title이 “Load”에서 “Finish”로 변경되는 화면이 있다고 합시다. 이 화면에 테스트 코드를 단순히 render
후 Finish
text를 찾는다면 에러가 발생합니다. 당연하죠. 변경하기 위해서는 2초에 시간이 필요하지만 테스트 코드는 동기적으로 진행하기 때문에 실행되는 과정에서는 text변경이 없습니다. ( 코드 예시는 아래에서 작성하겠습니다. )
결론은 당연히 비동기 작업을 테스트하기 위해서는 테스트 코드도 비동기로 작업이 필요하다는 것입니다. 아래에서 예시와 함께 정리해 봅시다.
비동기 작업을 고려한 테스트 코드
비동기 로직이 포함된 컴포넌트 코드
import React, { useCallback, useState } from "react";
const DelayTest = () => {
const [text, setText] = useState("Load");
const onToggle = useCallback(() => {
setTimeout(() => {
setText("Finish");
}, 1000);
}, []);
return (
<div>
<div data-testid="textData">{text}</div>
<button onClick={onToggle}>변경</button>
</div>
);
};
export default DelayTest;
잘못된 테스트 코드
import { fireEvent, render, screen } from "@testing-library/react";
import DelayTest from "../View/DelayTest";
describe("<DelayTest />", () => {
test("check 'Finish' text", () => {
render(<DelayTest />);
const button = screen.getByText("변경");
fireEvent.click(button);
const text = screen.getByTestId("textData");
expect(text).toHaveTextContent("Finish"); // error
});
});
text확인을 꼭 TestId를 통해 할 필요는 없습니다. 위 코드는 예상 코드와 실제 코드를 살펴보기 위해 약간 변형을 했습니다.
테스트 실행
테스트 결과 아직 text는 Load
인 것으로 보입니다. 그럼 이 코드를 수정해서 테스트를 통과해 보겠습니다.
findByXXX 사용
// 테스트 코드 수정
import { fireEvent, render, screen } from "@testing-library/react";
import DelayTest from "../View/DelayTest";
describe("<DelayTest />", () => {
test("check 'Finish' text", async () => {
render(<DelayTest />);
const button = screen.getByText("변경");
fireEvent.click(button);
await screen.findByText(
"Finish",
{},
{ timeout: 2000 }
);
});
});
수정된 코드를 확인해 봅시다. 비동기 과정을 동기처럼 처리하기 위해 익숙한 async await
를 사용합니다. 그리고 await 사용이 가능한 쿼리를 사용해야 합니다. 쿼리에 대한 정보는 아래 이미지와 다양한 쿼리를 확인해 주세요.
코드에서는 findByText
를 사용합니다. findByText는 (id: Matcher, options?: SelectorMatcherOptions, waitForElementOptions?: waitForOptions): Promise<HTMLElement>
구조를 가지고 있습니다.
예시 코드에서는 { timeout: 2000 }
옵션을 사용했습니다. 다양한 옵션은 있지만 글에 핵심에 맞게 timeout만 설명하도록 하겠습니다. 해당 옵션은 지연시간을 지정할 수 있습니다. 이 의미가 헷갈릴 수 있기 때문에 의미를 더 파악해 보도록 하겠습니다. 제가 헷갈렸던 의미로는 지정된 시간(예시 2초) 후에 text를 찾는다고 생각했습니다. 정확한 의미는 ‘Finish’ 텍스트를 찾는 과정을 2초 동안 기다린다는 의미입니다. 2초동안 일정 간격으로 텍스트를 찾는 행위를 반복하고 2초가 넘어갈 경우 에러가 발생합니다. 기본값은 1초입니다.
waitFor 사용
비동기를 처리하는 방식 중 waitFor방식을 사용하는 경우도 있습니다.
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import DelayTest from "../View/DelayTest";
describe("<DelayTest />", () => {
test("check 'Finish' text", async () => {
render(<DelayTest />);
const button = screen.getByText("변경");
fireEvent.click(button);
await waitFor(
() => {
screen.getByText("Finish");
},
{ timeout: 2000 }
);
});
});
waitFor
는 (callback: () => void | Promise<void>, options?: waitForOptions): Promise<void>
같은 구조를 가지고 있습니다. 이전과 마찬가지고 timeout
옵션을 지정할 수 있으며 기본값은 1초입니다.
findBy
와 차이점이라는 건 callback 함수를 받기 때문에 다양한 방식으로 활용할 수 있다는 점인데, 동작 원리는 동일합니다. 앞서 얘기한 것처럼 기다리는 게 아닌 반복 요청을 보내기 때문입니다. 공식문서에서도 작성된 내용입니다만, 눈으로 확인해 보기 위해 코드를 수정해 보겠습니다.
// waitFor 수정
await waitFor(
() => {
console.log("interval", new Date().getTime());
screen.getByText("Finish");
},
{ timeout: 10000 }
);
수정 후 테스트를 실행해 보면 공식문서에서 나온 것처럼 약 50ms간격으로 console.log
가 다수 찍히는 것을 확인할 수 있습니다. 또한 지정시간 10초가 아닌 해당 코드가 통과된 시점에 테스트가 종료되는 것을 볼 수 있습니다. 아래에 이미지를 확인해 주세요.
마무리
비동기 코드를 테스트할 수 있는 2가지 방법을 알아봤습니다.(waitForElementToBeRemoved()
등은 생략했습니다. ) findByxxx
, waitFor
방법 모두 지정된 timeout(기본값 1000ms) 동안 일정 Interval(기본값 50ms)을 가지고 확인하는 것을 봤습니다. 같은 기능을 2가지 방법으로 만들 수 있겠지만, 저는 두 방법에 용도가 완전히 다르기 때문에 필요한 상황에 맞게 선택하는 것이 바람직하다고 생각합니다. 개인적으로는 waitFor를 많이 사용할 것 같네요 :)