react-testing-library 를 사용한 리액트 컴포넌트 테스트

Minjun Kim·2019년 6월 4일
69
post-thumbnail

react-testing-library 에서는 Enzyme 과 달리 모든 테스트를 DOM 위주로 진행합니다. 그리고, 컴포넌트의 props 나 state 를 조회하는 일은 없습니다. 컴포넌트를 리팩토링하게 될 때에는, 주로 내부 구조 및 네이밍은 많이 바뀔 수 있어도 실제 작동 방식은 크게 바뀌지 않습니다. react-testing-library는 이 점을 중요시 여겨서, 컴포넌트의 기능이 똑같이 작동한다면 컴포넌트의 내부 구현 방식이 많이 바뀌어도 테스트가 실패하지 않도록 설계되었습니다. 추가적으로, Enzyme 은 엄청나게 다양한 기능을 제공하는 반면, react-testing-library 에는 정말 필요한 기능들만 지원을 해줘서 매우 가볍고, 개발자들이 일관성 있고 좋은 관습을 따르는 테스트 코드를 작성 할 수 있도록 유도해줍니다.

리액트 프로젝트 만들기

이번에 만들 컴포넌트들은 Enzyme 편에서 만든 컴포넌트들과 똑같습니다. 단, Enzyme 부분을 생략하고 바로 여기로 넘어오시는 분들을 위하여 프로젝트를 새로구성하겠습니다.

CRA 를 통하여 새 프로젝트를 만들어주세요.

$ yarn create rtl-tutorial
# 또는 npx create-react-app rtl-tutorial

설치

react-testing-library 를 프로젝트에 설치해봅시다.

$ yarn add react-testing-library jest-dom
# 또는 npm install --save react-testing-library jest-dom

jest-dom 은 jest 확장으로서, DOM 에 관련된 matcher 를 추가해줍니다.

VS Code 를 사용하는 경우 @types/jest 패키지도 설치하세요.

그 다음, src 디렉터리에 setupTests.js 파일을 생성해서 다음 코드를 입력해주세요.

src/setupTests.js

import 'react-testing-library/cleanup-after-each';
import 'jest-dom/extend-expect';

react-testing-library 에서는 리액트에서는 DOM 시뮬레이션을 위한 [JSDOM](https://github.com/jsdom/jsdom) 이라는 도구를 사용하여 document.body 에 리액트 컴포넌트를 렌더링합니다. clean-up-after-each 를 불러오면, 각 테스트 케이스가 끝날때마다 기존에 가상의 화면에 남아있는 UI 를 정리합니다.

추가적으로, 그 아래에는 jest-dom/extend-expect 를 불러와서 jest 에서 DOM 관련 matcher 를 사용 할 수 있게 해주었습니다.

첫번째 테스트 코드

username 과 name 을 props 로 넣어주면 이를 렌더링해주는 Profile 컴포넌트를 만들어봅시다.

src/Profile.js

import React from 'react';

const Profile = ({ username, name }) => {
  return (
    <div>
      <b>{username}</b>&nbsp;
      <span>({name})</span>
    </div>
  );
};

export default Profile;

이 컴포넌트를 만드셨으면 App 에서 렌더링해본 뒤 잘 보여지는지 먼저 확인해보세요.

src/App.js

import React from 'react';
import Profile from './Profile';

const App = () => {
  return <Profile username="velopert" name="김민준" />;
};

export default App;

App 을 수정하셨으면, yarn start (혹은 npm start) 를 입력하여 결과를 확인해보세요.

그리고, Profile 컴포넌트를 위한 테스트 코드를 작성해봅시다.

src/Profile.test.js

import React from 'react';
import { render } from 'react-testing-library';
import Profile from './Profile';

describe('<Profile />', () => {
  it('matches snapshot', () => {
    const utils = render(<Profile username="velopert" name="김민준" />);
    expect(utils.container).toMatchSnapshot();
  });
  it('shows the props correctly', () => {
    const utils = render(<Profile username="velopert" name="김민준" />);
    utils.getByText('velopert'); // velopert 라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText('(김민준)'); // (김민준) 이라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText(/김/); // 정규식 /김/ 을 통과하는 엘리먼트가 있는지 확인
  });
});

이제 yarn test (혹은 npm test) 명령어를 실행해서 작성한 테스트가 잘 통과하는지 확인해보세요.

react-testing-library 에서 컴포넌트를 렌더링 할 때에는 render() 라는 함수를 사용합니다. 이 함수가 호출되면 그 결과물 에는 DOM 을 선택 할 수 있는 다양한 쿼리들과 container 가 포함되어있는데요, 여기서 container 는 해당 컴포넌트의 최상위 DOM 을 가르킵니다. 이를 가지고 스냅샷 테스팅을 할 수도 있습니다.

그리고, 그 하단의 getByText 는 쿼리함수라고 부르는데요 이 함수를 사용하면 텍스트를 사용해서 원하는 DOM 을 선택 할 수 있습니다. 이에 대해서는 잠시 후 더 자세히 알아보겠습니다.

스냅샷 테스팅

스냅샷 테스팅이란, 렌더링된 결과가 이전에 렌더링한 결과와 일치하는지 확인하는 작업을 의미합니다.

코드를 저장하면 src/__snapshots__/Profile.test.js.snap 라는 파일이 다음과 같이 만들어질 것입니다.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Profile /> matches snapshot 1`] = `
<div>
  <div>
    <b>
      velopert
    </b>
     
    <span>
      (
      김민준
      )
    </span>
  </div>
</div>
`;

컴포넌트가 렌더링됐을 때 이 스냅샷과 일치하지 않으면 테스트가 실패합니다. 만약에 스냅샷을 업데이트 하고싶다면 테스트가 실행되고 있는 콘솔 창에서 u 키를 누르면 됩니다.

다양한 쿼리

render 함수를 실행하고 나면 그 결과물 안에는 다양한 쿼리 함수들이 있는데요, 이 쿼리 함수들은 react-testing-library 의 기반인 dom-testing-library 에서 지원하는 함수들입니다.

이 쿼리 함수들은 VariantQueries 의 조합으로 네이밍이 이루어져있는데요, 우선 Varient 에는 어떤 종류들이 있는지 봅시다.

Variant

getBy

getBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 없으면 에러가 발생합니다.

getAllBy

getAllBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 하나도 없으면 에러가 발생합니다.

queryBy

queryBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.

queryAllBy

queryAllBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.

findBy

findBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.

findAllBy

findBy* 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.

Queries

ByLabelText

ByLabelText 는 label 이 있는 input 의 label 내용으로 input 을 선택합니다.

<label for="username-input">아이디</label>
<input id="username-input" />

const inputNode = getByLabelText('아이디');

ByPlaceholderText

ByPlaceholderText 는 placeholder 값으로 input 및 textarea 를 선택합니다.

<input placeholder="아이디" />;

const inputNode = getByPlaceholderText('아이디');

ByText

ByText는 엘리먼트가 가지고 있는 텍스트 값으로 DOM 을 선택합니다.

<div>Hello World!</div>;

const div = getByText('Hello World!');

참고로, 텍스트 값에 정규식을 넣어도 작동합니다.

const div = getByText(/^Hello/);

ByAltText

ByAltTextalt 속성을 가지고 있는 엘리먼트 (주로 img) 를 선택합니다.

<img src="/awesome.png" alt="awesome image" />;

const imgAwesome = getByAltText('awesomse image');

ByTitle

ByTitletitle 속성을 가지고 있는 DOM 혹은 title 엘리먼트를 지니고있는 SVG 를 선택 할 때 사용합니다.

title 속성은 html 에서 툴팁을 보여줘야 하는 상황에 사용하곤 합니다.

<p>
  <span title="React">리액트</span>는 짱 멋진 라이브러리다.
</p>

<svg>
  <title>Delete</title>
  <g><path/></g>
</svg>

const spanReact = getByTitle('React');
const svgDelete = getByTitle('Delete');

ByDisplayValue

ByDisplayValueinput, textarea, select 가 지니고 있는 현재 값을 가지고 엘리먼트를 선택합니다.

<input value="text" />;

const input = getByDisplayValue('text');

ByRole

ByRole은 특정 role 값을 지니고 있는 엘리먼트를 선택합니다.

<span role="button">삭제</span>;

const spanRemove = getByRole('button');

ByTestId

ByTestId 는 다른 방법으로 못 선택할때 사용하는 방법인데요, 특정 DOM 에 직접 test 할 때 사용할 id 를 달아서 선택하는 것을 의미합니다.

<div data-testid="commondiv">흔한 div</div>;

const commonDiv = getByTestId('commondiv');

!> 주의: camelCase 가 아닙니다. 값을 설정할때 data-testid="..." 이렇게 설정하셔야합니다. 추가적으로, ByTestId 는 다른 방법으로 선택할 수 없을때에만 사용해야합니다.

어떤 쿼리를 사용해야 할까?

쿼리의 종류가 정말 많죠? 그렇다면, 어떤 쿼리를 우선적으로 사용해야 할까요? 매뉴얼 에서는 다음 우선순위를 따라서 사용하는것을 권장하고있습니다.

  1. getByLabelText
  2. getByPlaceholderText
  3. getByText
  4. getByDisplayValue
  5. getByAltText
  6. getByTitle
  7. getByRole
  8. getByTestId

그리고, DOM 의 querySelector 를 사용 할 수도 있는데요, 이는 지양해야합니다. 차라리 data-testid 를 설정하는것이 좋습니다.

const utils = render(<MyComponent />);
const element = utils.container.querySelector('.my-class');

Counter 컴포넌트 테스트 코드 작성하기

이번에는 Counter 컴포넌트를 만들고, 이를 위한 테스트 코드를 작성해봅시다.

먼저 Counter.js 파일을 생성해서 다음 코드를 작성하세요.

src/Counter.js

import React, { useState, useCallback } from 'react';

const Counter = () => {
  const [number, setNumber] = useState(0);

  const onIncrease = useCallback(() => {
    setNumber(number + 1);
  }, [number]);

  const onDecrease = useCallback(() => {
    setNumber(number - 1);
  }, [number]);

  return (
    <div>
      <h2>{number}</h2>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
};

export default Counter;

그리고, 이 컴포넌트를 App 에서 렌더링하여 잘 작동하는지 확인해보세요.

import React from 'react';
import Counter from './Counter';

const App = () => {
  return <Counter />;
};

export default App;

잘 보여졌나요? 그럼 Counter를 위한 테스트 코드를 작성해보겠습니다.

src/Counter.test.js

import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import Counter from './Counter';

describe('<Counter />', () => {
  it('matches snapshot', () => {
    const utils = render(<Counter />);
    expect(utils.container).toMatchSnapshot();
  });
  it('has a number and two buttons', () => {
    const utils = render(<Counter />);
    // 버튼과 숫자가 있는지 확인
    utils.getByText('0');
    utils.getByText('+1');
    utils.getByText('-1');
  });
  it('increases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('+1');
    // 클릭 이벤트를 두번 발생시키기
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('2'); // jest-dom 의 확장 matcher 사용
    expect(number.textContent).toBe('2'); // textContent 를 직접 비교
  });
  it('decreases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('-1');
    // 클릭 이벤트를 두번 발생시키기
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('-2'); // jest-dom 의 확장 matcher 사용
  });
});

이벤트 다루기

여기서 fireEvent() 라는 함수를 불러와서 사용했는데요, 이 함수는 이벤트를 발생시켜줍니다. 사용법은 다음과 같습니다.

fireEvent.이벤트이름(DOM, 이벤트객체);

클릭 이벤트의 경우엔 이벤트객체를 따로 넣어주지 않아도 되지만, 예를 들어서 change 이벤트의 경우엔 다음과 같이 해주어야합니다.

fireEvent.change(myInput, { target: { value: 'hello world' } });

이제 react-testing-library 의 주요 기능을 벌써 다 배우셨습니다!

아직 다루지 않은 내용은 비동기 작업인데요, 이에 대한 내용은 나중에 이어질 섹션에서 다루게 됩니다.

profile
CEO @ Chaf Inc. 사용자들이 좋아하는 프로덕트를 만듭니다.

18개의 댓글

comment-user-thumbnail
2019년 6월 4일

setupTests.js에서 import에러가 나는 경우는 어떻게 해결해야 할까요?

4개의 답글
comment-user-thumbnail
2019년 6월 5일

[오탈자 수정]
yarn create rtl-tutorial -> yarn create react-app rtl-tutorial

답글 달기
comment-user-thumbnail
2019년 7월 10일

Thx

답글 달기
comment-user-thumbnail
2019년 7월 18일

위의 예제중 getByText 를사용하는 테스트 코드가 모두

(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).

라고 뜨며 실패가 뜨는데 문구대로 AllBy 를 쓰면 테스트를 성공하고 있습니다...
AllBy 와 일반 By 차이는 하나를 선택하나 멀티로 선택하냐의 차이로 알고있는데 왜 저런 실패가 발생하는지 궁금합니다 ㅠㅠ

1개의 답글
comment-user-thumbnail
2019년 8월 28일

Counter 컴포넌트를 Dumb / Smart 컴포넌트로 구별지어 Test 코드를 작성할 경우
Dumb Component Test: 'matches snapshot', 'has a number and two buttons'
Smart Component Test: 'increases', 'decrease'
이렇게 구분지어 테스트하면 될까요?

답글 달기
comment-user-thumbnail
2019년 9월 24일

FAIL src/Profile.test.js
● Test suite failed to run

🚨  react-testing-library has moved to @testing-library/react. Please uninstall react-testing-library and install @testing-library/react instead, or use an older version of react-testing-library. Learn more about this change here: https://github.com/testing-library/dom-testing-library/issues/260 Thanks! :)

라고 뜨는군요
예제 업데이트가 필요할거 같네요 @velopert

답글 달기
comment-user-thumbnail
2019년 9월 29일

On Main Article

standard: 29.09.2019

invalid : $ yarn add react-testing-library jest-dom
valid: $ yarn add @testing-library/jest-dom @testing-library/react -D

invalid :

import 'react-testing-library/cleanup-after-each';
import 'jest-dom/extend-expect';

valid: import '@testing-library/jest-dom/extend-expect';

After setting valid, you can see "devDependencies" package.json

"@testing-library/jest-dom": "^4.1.0",
"@testing-library/react": "^9.2.0",
1개의 답글
comment-user-thumbnail
2021년 2월 1일

render함수는 는 더 이상 react-testing-library 에서 임포트 하지 않습니다.

https://github.com/testing-library/dom-testing-library/issues/260

해당 예제를 따라할 실 분은 먼저 @testing-library/react 설치 후 아래를 진행 해주시면 됩니다.

아래 명령어 입력하여 @testing-library/react 설치

yarn add -D @testing-library/react @testing-library/jest-dom

#setupTests.js 파일 수정
import '@testing-library/react';
import '@testing-library/jest-dom';

Counter.test.js 파일 수정

import { render, fireEvent } from 'react-testing-library';
위의 import 구문을 아래 구문으로 대체
import { render, fireEvent } from "@testing-library/react";

답글 달기