티스토리 뷰

본 포스팅은 아래 글을 토대로 정리된 내용입니다 :) 

 

Common mistakes with React Testing Library

Some mistakes I frequently see people making with React Testing Library.

kentcdodds.com

위 글에서는 다음과 같은 중요도에 따라 레이블을 지정했습니다.

  • 낮음(⭐) : 이것은 대부분 개인적인 의견일 뿐이므로, 자유롭게 무시해도 괜찮을 것이다. 
  • 중간(⭐⭐) : 버그를 경험하거나, 신뢰도를 잃거나, 필요치 않은 작업을 하게 될지도 모른다.
  • 높음(⭐⭐⭐) : 꼭 이 조언을 듣자! 신뢰도가 낮거나 문제가 있는 테스트가 있을 수 있다.

 

1. Testing Library ESLint 플러그인을 사용하지 않음 ⭐⭐

→ Testing library에 ESLint 플러그인을 설치하고 사용하자

일반적인 실수 중 몇 가지를 피하려면 공식 ESLint 플러그인이 많은 도움이 될 수 있습니다. 적용하게 되면 규칙의 출처를 명시해주며 권장 규칙에 대해 알려줍니다.

 

CRA하게 되면 ESLint는 이미 설치가 되어 있고, 테스팅 라이브러리와 jest-dom에 ESLint 플러그인을 설치해주어야 합니다.

$ npm i eslint-plugin-testing-library eslint-plugin-jest-dom

그리고 package.json 파일에서 "eslintConfig" 부분을 모두 지워주고 프로젝트 폴더 바로 아래에 `.eslintrc.json` 파일을 만들어줍니다. 아래는 예시 코드입니다.

 

📜 .eslintrc.json 파일

{
  "plugins": ["jest-dom", "testing-library"],
  "extends": [
    "react-app",
    "react-app/jest",
    "plugin:testing-library/react",
    "plugin:jest-dom/recommended"
  ]
}

 

추가적으로 vscode 내에서 아래 설정도 같이 해주면 Ctrl + S 해서 저장할 때 ESLint 규칙에 맞게 자동 수정됩니다.

{
  "editor.codeActionsOnSave": { "source.fixAll.eslint": true },
}

 

결론적으로, Testing Library 용 ESLint 플러그인을 통해 권장 규칙을 쉽게 적용할 수 있습니다.

 

2. `wrapper`에서 반환 값에 대한 변수 이름으로 사용 `render` ⭐ 

→ render로 부터 필요한 것을 구조 분해하거나, (wrapper 대신) view 라고 부르자

// ❌
const wrapper = render(<Example prop="1" />)
wrapper.rerender(<Example prop="2" />)

// ✅
const {rerender} = render(<Example prop="1" />)
rerender(<Example prop="2" />)

wrapper라는 이름은 enzyme의 오래된 코드이며 여기선 필요하지 않습니다. render의 리턴 값은 아무것도 "감싸지(wrapping)" 않습니다. 필요한 메서드들은 구조 분해 할당하여 가져와 사용합시다.

 

3. `cleanup` 사용하기 ⭐⭐

→ cleanup을 사용하지 말자!

// ❌
import { render, screen, cleanup } from "@testing-library/react";
afterEach(cleanup);
// ✅
import { render, screen } from "@testing-library/react";

 

오랜 시간 동안 `cleanup`은 자동으로 이루어지고 (대부분 주요 테스팅 프레임워크에서 지원됨) 더는 거기에 대해 걱정할 필요가 없습니다. 따라서 별도로 사용하지 않아도 됩니다.

 

4. `screen` 사용 안 하기 ⭐⭐

→ 가독성이 좋은 screen을 사용하자

RTL에서 `screen`은 테스트할 컴포넌트의 요소를 선택하고 그 요소와 상호작용을 할 수 있게 해줍니다. screen 객체는 테스트에서 사용할 수 있는 다양한 메서드와 속성을 제공합니다(자동완성도 제공합니다).

// ❌
const { getByRole } = render(<Example />);
const errorMessageNode = getByRole("alert");
// ✅
render(<Example />);
const errorMessageNode = screen.getByRole("alert");

`screen`은 DOM Testing Library 6.11.0 버전에서 추가되었으며, render를 가져오는 곳과 동일한 import문에서 나오는데요.

import { render, screen } from "@testing-library/react";

 

 

`screen`을 사용해서 얻는 이점은 필요한 쿼리를 추가/제거할 때 `render`를 호출해 구조 분해할 필요가 없다는 것인데요. 만약 screen 객체를 사용하지 않으면 다음과 같은 방법은 테스팅할 컴포넌트 요소를 찾아야 합니다.

const { getByText, getByRole, getByTestId } = render(<MyComponent />);
  
// getByText를 사용하여 텍스트를 가진 요소 선택 및 검증
const heading = getByText('Hello, World!');
expect(heading).toBeInTheDocument();

// getByRole을 사용하여 역할을 가진 요소 선택 및 검증
const button = getByRole('button');
expect(button).toHaveAttribute('type', 'submit');

// getByTestId를 사용하여 data-testid를 가진 요소 선택 및 검증
const input = getByTestId('my-input');
expect(input).toHaveValue('');

혹은 `container`를 render에서 구조 분해해와 여기에서 메서드를 사용하거나 하는 방법도 존재합니다.

const { container } = render(<MyComponent />);
  
// container를 사용하여 특정 요소 검색 및 검증
const heading = container.querySelector('h1');
expect(heading).toBeInTheDocument();

// container를 사용하여 특정 요소 검색 및 속성 검증
const button = container.querySelector('button[type="submit"]');
expect(button).toBeInTheDocument();

 

이 중에서 screen 객체가 가장 간편한 것 같습니다 :)

 

5. 잘못된 단언문 사용 ⭐⭐⭐

→ @testing-library/jest-dom을 사용하자!

const button = screen.getByRole('button', {name: /disabled button/i})

// ❌
expect(button.disabled).toBe(true)
// error message:
//  expect(received).toBe(expected) // Object.is equality
//
//  Expected: true
//  Received: false

// ✅
expect(button).toBeDisabled()
// error message:
//   Received element is not disabled:
//     <button />

위에서 사용한 toBeDisabled 단언문은 jest-dom에 있습니다. 훨씬 나은 에러 메시지를 받을 수 있으므로 jest-dom을 사용하는 것을 추천합니다.

즉, 내가 테스팅하고 싶은 요소의 속성까지도 지정하여 테스팅을 하기 보다는 요소까지만 찾고, jest-dom의 메서드를 이용하여 테스팅하는 것을 추천한다는 말과 같습니다.

 

6. `act`로 불필요하게 감싸기 ⭐⭐

→ 언제 act가 필요한지 배우고 불필요하게 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는 React Testing Library에서 제공하는 함수로, 비동기적인 동작이나 상태 업데이트와 관련된 테스트 코드를 작성할 때 사용됩니다. 그래서 `act`를 사용하여 테스트 코드 안에서 컴포넌트와 상호작용하는 동안 발생하는 React 업데이트를 동기적으로 처리하고 검증할 수 있는데요.

 

간혹 `act`와 관련된 에러가 뜨곤 하는데요. 이때 무작정 act로 감싸버리는 경우가 있습니다.  언제 act가 필요한지 배우고 불필요하게 act로 감싸지 않도록 해야 합니다.

 

7. 잘못된 쿼리 사용하기 ⭐⭐⭐

"어떤 쿼리를 써야할까요" 가이드를 읽고 추천에 따르자!

// ❌
// DOM이 동작한다고 가정하기:
// <label>Username</label><input data-testid="username" />
screen.getByTestId("username");
// ✅
// 레이블을 연결하고 타입을 설정해 DOM을 접근가능하도록 변경
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole("textbox", { name: /username/i });

RTL 공식 문서에 다양한 쿼리들이 소개되어 있는데요. 관련되어 흔히 하는 실수들에 대해 알아봅시다.

 

container를 사용하여 요소 쿼리를 사용하지 말자

// ❌
const { container } = render(<Example />);
const button = container.querySelector(".btn-primary");
expect(button).toHaveTextContent(/click me/i);
// ✅
render(<Example />);
screen.getByRole("button", { name: /click me/i });

유저들이 UI와 상호작용할 수 있는 `querySelector`를 사용해서 쿼리하면 많은 신뢰도를 잃고, 테스트가 읽기 힘들어질 것입니다. `querySelector`는 우선 RTL에서제공하는 메서드는 아니기 때문에 이후의 컴포넌트의 검증하는 단계에서 용이하지 않을 수 있습니다.

따라서 RTL에서 제공하는 `render`와 `screen`객체를 통해 의도가 명확한 테스트 코드를 작성하고 컴포넌트의 실제 동작과 동기화된 테스트를 수행할 수 있습니다.

 

실제 텍스트를 쿼리 안에서 이용하자

테스트 ID나 그 외 기술적인 것들을 사용하는 대신 실제 텍스트로 쿼리하는 것을 추천하는지에 대해 이야기해보려고 합니다.

// ❌
screen.getByTestId("submit-button");
// ✅
screen.getByRole("button", { name: /submit/i });

 

RTL의 핵심 철학은 사용자의 관점에서 컴포넌트를 테스트하는 것인데요. 위처럼 실제로 사용자가 화면에서 보게 될 텍스트에 기반하여 요소를 선택하고 검증함으로써 사용자의 경험을 더욱 잘 반영할 수 있습니다. 이는 테스트 코드의 가독성을 높이고, 테스트의 목적과 의도를 분명하게 전달할 수 있습니다.

 

`*ByRole`를 대부분 사용하자

ByRole 말고도 ByText 등 여러 방법으로 테스팅하고 싶은 컴포넌트를 찾을 수 있지만, ByRole을 사용하자는 이유는 다음과 같습니다.

  • 사용자 관점의 테스트 : RTL은 사용자의 관점에서 컴포넌트를 테스트하는 것을 강조하는데, 사용자는 버튼, 링크, 필드 등과 같은 특정한 역할을 가진 요소를 인식하고 상호작용하므로, `ByRole`을 통해 역할에 따라 요소를 선택하고 검증함으로써 사용자의 경험을 더욱 잘 반영할 수 있다
  • 접근성 검증 : 특정 역할을 가진 요소를 선택하고 해당 요소의 상태나 속성을 검증함으로써 접근성 요구사항도 검증이 가능하다
  • 가독성과 사용성 : `ByRole`을 사용하면 컴포넌트를 선택하는 데 역할 기반의 쿼리를 사용함으로써, 테스트 코드의 의도가 분명하게 전달되어 코드를 이해하고 유지보수할 때도 도움이 됩니다. 
// DOM이 동작한다고 가정하기
// <button><span>Hello</span> <span>World</span></button>
screen.getByText(/hello world/i);
// ❌ 아래와 같은 에러와 함께 실패함:
// 다음과 같은 텍스트를 가진 엘리먼트를 찾을 수 없습니다: /hello world/i.
// 이것은 다수의 엘리먼트들에 의해 텍스트가 부서졌기 때문일 수 있습니다.
// 이런 경우, 텍스트 matcher를 더 유연하게 만들기 위해 함수를 제공할 수 있습니다.
screen.getByRole("button", { name: /hello world/i });
// ✅ 동작함!

 

8. @testing-library/user-event 사용하지 않기 ⭐⭐

→ 가능한 fireEvent보다 @testing-library/user-event를 사용하자!

// ❌
fireEvent.change(input, { target: { value: "hello world" } });
// ✅
userEvent.type(input, "hello world");

 

`@testing-library/user-event`는 `fireEvent`를 기반으로 빌드된 패키지지만, 사용자 상호작용과 더 유사한 여러 메서드들을 제공합니다. 둘 다 실제 사용자의 인터렉션처럼 무언가를 발생시키긴 하지만, user-event는 실제 사용자의 인터렉션을 모방한다고 보고, fireEvent는 특정 이벤트를 강제로 발생시키는 것으로 볼 수 있습니다.

 

따라서 user-event가 조금 더 추상화된 방식으로 이벤트를 다루게 되어 사용자의 실제 동작을 정확히 모방해야 하는 경우에는 user-event를 사용하는 것이 더 적합합니다.

 

9. 존재 여부를 확인하는 경우 외의 모든 곳에 `query` 변형을 사용하기 ⭐⭐⭐

→ query 변형은 엘리먼트를 찾을 수 없는 단언을 할 때만 사용!

쿼리들은 query 변형이 노출되는 유일한 이유는 쿼리와 일치하는 엘리먼트가 없을 때 에러를 발생하지 않고 호출할 수 있는 함수를 갖기 때문입니다. (아무 요소도 찾을 수 없다면 null을 반환합니다.)

query 변형이 유용한 유일한 이유는 컴포넌트가 페이지에 렌더링 되지 않았는지 확인하는 것입니다.

 

예를 들어, hover를 하기 전에는 그 컴포넌트가 렌더링이 되지 않은 상태인지를 테스팅할 때? 화면에 없을 것을 단언하고 싶을 때만 query 변형을 사용하도록 합시다.

 

10. find*로 쿼리할 수 있는 컴포넌트를 waitFor를 사용해서 기다리기 ⭐⭐⭐

→ 즉시 사용할 수 없는 무언가를 쿼리하고 싶을 때는 언제든지 find*를 사용하자

// ❌
const submitButton = await waitFor(() =>
  screen.getByRole("button", { name: /submit/i })
);
// ✅
const submitButton = await screen.findByRole("button", { name: /submit/i });

위 두 줄 코드는 근본적으로는 같지만 (find* 쿼리는 내부에서 waitFor을 사용), 두 번째 코드가 더 간단하고 더 나은 에러메세지를 받을 수 있습니다.

 

11.  `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);

`waitFor`의 목적은 특정한 것이 일어날 때까지 기다리게 하는 것입니다. 그래서 요소를 찾거나, 찾지 못해서 타임아웃이 되거나 두 가지 결과로 나뉘게 되는데요. 만약 빈 콜백을 넘겨준다면 다음과 같은 단점이 있습니다.

  • 의도하지 않은 테스트 통과 : `waitFor`의 콜백 함수가 비어있는 경우, 테스트는 즉시 통과될 수 있다. 비동기 작업의 완료 여부를 확인하지 않고 테스트가 통과되는 것을 의미.
  • 무한 대기 가능성 : 빈 콜백을 사용하면 `waitFor`은 콜백 함수가 끝나지 않을 때까지 계속 기다리게 되는데, 이때 계속 대기하므로 이는 테스트 실행 시간을 늘리고 전반적인 성능을 저하시킬 수 있다

 

12. `waitFor`에 사이드 이펙트 수행하기 ⭐⭐⭐

→ `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`은 수행한 작업과 단언문 전달 사이에 비동기로 인한 인터렉션을 테스트 하기 위한 것입니다. 따라서, `waitFor` 안에서 사이드 이펙트 코드를 작성하면 비동기 작업 완료와는 상관없는 다른 작업이 동시에 실행될 수 있으므로 테스트 결과를 예측하기 어려워집니다. 즉, 테스트 의도의 명확성과 유지보수성을 위해 비동기 작업과 관련된 테스트와 아닌 것을 분리하는 것이 좋습니다.

 

+ 비동기 메서드 - findBy과 waitFor 구분해서 사용하기

비동기 메서드를 사용해야 하는 상황들은, 요소가 사용자 상호작용 후에만 표시된다거나, 사용자 작업 후 요소가 사라지는 경우를 테스트해야 한다거나 하는 것들입니다. 이를 위해 두 가지의 메서드가 존재하는데요.

 

1. `findBy` 쿼리 사용

특정 컴포넌트를 가져오는 `getBy'와 업데이트되는 요소나 조건을 기다리는 `waitFor`의 조합입니다. 만약 버튼에 대한 사용자 인터렉션이 있다고 가정하고, 이 버튼이 클릭되면 그때 화면에 보이는 컴포넌트에 대해 테스트하고 싶다고 해보겠습니다.

const button = screen.getByRole('button');
button.click();
expect(await screen.findByText('Clicked once')).toBeInTheDocument();
button.click();
expect(await screen.findByText('Clicked twice')).toBeInTheDocument();

이를 보았을 때, findBy는 주로 비동기적으로 렌더링되는 요소를 찾기 위해 사용됩니다. 특정 요소가 기다릴 때까지 기다린 다음 해당 요소를 반환합니다.

 

2., waitFor 사용

`findBy`의 경우 어떤 함수가 실행되기를 기다려야 하기 때문에 쿼리를 활용할 수 없는 경우가 있습니다. 아니면 다음 일을 하기 전에 시간이 좀 지나야할 수도 있습니다. 이것이 바로 `waitFor` 기능이 유용한 곳인데요.

await waitFor(() => {
  expect(mockCall).toHaveBeenCalledTimes(1);
});

waitFor은 주로 비동기적으로 업데이트되는 요소나 조건을 기다리기 위해 사용됩니다. 그리고 콜백 함수 내에서 조건을 검사하므로 어떤 요소에 대해서도 사용할 수 있습니다. 

 


참고문서

728x90
LIST
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함