React Testing Library의 일반적인 실수
원문: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library
kent의 글을 읽고 정리하고 싶은 부분을 정리하였다.
중요도 높음
queryBy~는 존재 여부를 확인하는 경우 이외에 사용하지 말것.
// ❌
expect(screen.queryByRole("alert")).toBeInTheDocument();
// ✅
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
쿼리들의 queryBy
변형이 노출되는 유일한 이유는 일치하는 엘리먼트가 없을 때 에러를 발생하지 않고 호출할 수 있는 함수를 갖기 때문이다. (아무 요소도 찾을 수 없다면 null
을 반환한다.)
waitFor을 남용하지 말것
// ❌
const submitButton = await waitFor(() =>
screen.getByRole("button", { name: /submit/i })
);
// ✅
const submitButton = await screen.findByRole("button", { name: /submit/i });
위의 코드는 의미상으론 동일하지만 두번째 코드가 더 간단하고 나은 에러 메세지를 받을 수 있다. (find* 쿼리에서 내부적으로 waitFor을 사용하고 있다! ❗️)
waitFor에 빈 콜백 넘기지 말것
// ❌
await waitFor(() => {});
expect(window.fetch).toHaveBeenCalledWith("foo");
expect(window.fetch).toHaveBeenCalledTimes(1);
// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith("foo"));
expect(window.fetch).toHaveBeenCalledTimes(1);
await wairFor(() => {});
을 하지 말것. 보통 이러한 코드를 작성하는 것이 이벤트 루프의 한틱을 기다리기 위해서 작성하는데 비동기 로직을 리팩토링하면 실패 실패하는 로직이기 때문에 명확한 단언문을 사용할 것.
waitFor에 사이드 이펙트를 수행하지 말것
// ❌
await waitFor(() => {
fireEvent.keyDown(input, { key: "ArrowDown" });
expect(screen.getAllByRole("listitem")).toHaveLength(3);
});
// ✅
fireEvent.keyDown(input, { key: "ArrowDown" });
await waitFor(() => {
expect(screen.getAllByRole("listitem")).toHaveLength(3);
});
waitFor은 수행한 작업과 단언문 전달 사이에 비결정적 시간이 있는 테스트를 위한 것입니다.
이러한 이유로, 콜백은 비결정적 횟수와 빈도로 호출될(혹은 에러 검사할) 수 있습니다.(DOM이 변경될 때 마다도 호출됩니다). 따라서 이것은 사이드 이펙트가 여러 번 실행될 수 있다는 뜻입니다!
올바른 쿼리를 사용할 것.
- container를 사용하여 요소 쿼리: querySelector를 이용하여 쿼리하지 말 것.
- 웬만하면 텍스트로 쿼리할 것: 테스트 ID등을 사용하는 것을 최대한 지양하고
getByRole
과 기본 locale을 사용하여 쿼리하세요. getByRole
을 이용하기 위해서 role 속성을 직접 추가하지 마세요.- 손쉬운 접근성 속성은 좋든 싫든 불필요할 뿐 아니라, 스크린 리더와 서비스 이용자들을 혼란스럽게 만듭니다. 접근성 속성은 semantic 태그로 만족시키지 못할 때만 사용하세요.
Note: “role”을 통해
input
에 접근성을 향상시키기 위해선type
속성을 지정해야 합니다!
중요도 중간
cleanup을 사용하지 말것.
// ❌
import { render, screen, cleanup } from "@testing-library/react";
afterEach(cleanup);
// ✅
import { render, screen } from "@testing-library/react";
cleanup은 오랜 시간동안 자동으로 이루어지도록 리팩토링 되었습니다. 따라서 테스트 코드를 작성할 때 걱정할 필요가 없습니다.
screen을 사용할 것.
// ❌
const { getByRole } = render(<Example />);
const errorMessageNode = getByRole("alert");
// ✅
render(<Example />);
const errorMessageNode = screen.getByRole("alert");
screen은 6.11.0에 새롭게 추가되었으며, render를 가져오는 동일한 import문에서 나옵니다. screen
을 사용해서 얻는 이점은 더는 필요한 쿼리를 추가/제거 할 때 render
를 호출해 최신 상태로 구조 분해할 필요가 없다는 것입니다. 당신이 필요한 건 screen
을 입력하는 것뿐입니다. 그리고 에디터의 자동완성 마법이 나머지를 처리합니다.
불필요하게 act를 감싸지 않을 것.
// ❌
act(() => {
render(<Example />);
});
const input = screen.getByRole("textbox", { name: /choose a fruit/i });
act(() => {
fireEvent.keyDown(input, { key: "ArrowDown" });
});
// ✅
render(<Example />);
const input = screen.getByRole("textbox", { name: /choose a fruit/i });
fireEvent.keyDown(input, { key: "ArrowDown" });
경고문을 지우기 위해 무분별하게 무분별하게 act로 감싸지 말고 정확히 왜 필요한 지에 대해 배우고 정확히 감싸도록 하세요.
@testing-library/user-event
를 사용할 것.
// ❌
fireEvent.change(input, { target: { value: "hello world" } });
// ✅
userEvent.type(input, "hello world");
@testing-library/user-event
는 fireEvent
의 기반으로 빌드된 패키지지만, 사용자 상호작용과 더 유사한 여러 메서드들을 제공합니다. 앞의 예제에서, fireEvent.change
는 단순히 input의 하나의 변경 이벤트를 트리거할 것입니다. 하지만 type
호출은 문자마다 keyDown
, keyPress
, keyUp
이벤트들을 트리거합니다. 이쪽이 훨씬 실제 사용자 상호작용과 유사하죠. 이것은 변경 이벤트를 수신하지 않는 라이브러리와도 잘 동작한다는 이점이 있습니다.
따라서 가능한 fireEvent
보다 userEvent
를 사용하세요.