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이 변경될 때 마다도 호출됩니다). 따라서 이것은 사이드 이펙트가 여러 번 실행될 수 있다는 뜻입니다!

올바른 쿼리를 사용할 것.

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 호출은 문자마다 keyDownkeyPresskeyUp 이벤트들을 트리거합니다. 이쪽이 훨씬 실제 사용자 상호작용과 유사하죠. 이것은 변경 이벤트를 수신하지 않는 라이브러리와도 잘 동작한다는 이점이 있습니다.
따라서 가능한 fireEvent보다 userEvent를 사용하세요.