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>
<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
에서 지원하는 함수들입니다.
이 쿼리 함수들은 Variant
와 Queries
의 조합으로 네이밍이 이루어져있는데요, 우선 Varient
에는 어떤 종류들이 있는지 봅시다.
getBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 없으면 에러가 발생합니다.
getAllBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 하나도 없으면 에러가 발생합니다.
queryBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.
queryAllBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.
findBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.
findBy*
로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.
ByLabelText
는 label 이 있는 input 의 label 내용으로 input 을 선택합니다.
<label for="username-input">아이디</label>
<input id="username-input" />
const inputNode = getByLabelText('아이디');
ByPlaceholderText
는 placeholder 값으로 input 및 textarea 를 선택합니다.
<input placeholder="아이디" />;
const inputNode = getByPlaceholderText('아이디');
ByText
는 엘리먼트가 가지고 있는 텍스트 값으로 DOM 을 선택합니다.
<div>Hello World!</div>;
const div = getByText('Hello World!');
참고로, 텍스트 값에 정규식을 넣어도 작동합니다.
const div = getByText(/^Hello/);
ByAltText
는 alt
속성을 가지고 있는 엘리먼트 (주로 img
) 를 선택합니다.
<img src="/awesome.png" alt="awesome image" />;
const imgAwesome = getByAltText('awesomse image');
ByTitle
은 title
속성을 가지고 있는 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
는 input
, textarea
, select
가 지니고 있는 현재 값을 가지고 엘리먼트를 선택합니다.
<input value="text" />;
const input = getByDisplayValue('text');
ByRole
은 특정 role
값을 지니고 있는 엘리먼트를 선택합니다.
<span role="button">삭제</span>;
const spanRemove = getByRole('button');
ByTestId
는 다른 방법으로 못 선택할때 사용하는 방법인데요, 특정 DOM 에 직접 test 할 때 사용할 id 를 달아서 선택하는 것을 의미합니다.
<div data-testid="commondiv">흔한 div</div>;
const commonDiv = getByTestId('commondiv');
!> 주의: camelCase 가 아닙니다. 값을 설정할때 data-testid="..."
이렇게 설정하셔야합니다. 추가적으로, ByTestId
는 다른 방법으로 선택할 수 없을때에만 사용해야합니다.
쿼리의 종류가 정말 많죠? 그렇다면, 어떤 쿼리를 우선적으로 사용해야 할까요? 매뉴얼 에서는 다음 우선순위를 따라서 사용하는것을 권장하고있습니다.
그리고, DOM 의 querySelector
를 사용 할 수도 있는데요, 이는 지양해야합니다. 차라리 data-testid
를 설정하는것이 좋습니다.
const utils = render(<MyComponent />);
const element = utils.container.querySelector('.my-class');
이번에는 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 의 주요 기능을 벌써 다 배우셨습니다!
아직 다루지 않은 내용은 비동기 작업인데요, 이에 대한 내용은 나중에 이어질 섹션에서 다루게 됩니다.
위의 예제중 getByText 를사용하는 테스트 코드가 모두
(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).
라고 뜨며 실패가 뜨는데 문구대로 AllBy 를 쓰면 테스트를 성공하고 있습니다...
AllBy 와 일반 By 차이는 하나를 선택하나 멀티로 선택하냐의 차이로 알고있는데 왜 저런 실패가 발생하는지 궁금합니다 ㅠㅠ
Counter 컴포넌트를 Dumb / Smart 컴포넌트로 구별지어 Test 코드를 작성할 경우
Dumb Component Test: 'matches snapshot', 'has a number and two buttons'
Smart Component Test: 'increases', 'decrease'
이렇게 구분지어 테스트하면 될까요?
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
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",
https://github.com/testing-library/dom-testing-library/issues/260
해당 예제를 따라할 실 분은 먼저 @testing-library/react 설치 후 아래를 진행 해주시면 됩니다.
yarn add -D @testing-library/react @testing-library/jest-dom
#setupTests.js 파일 수정
import '@testing-library/react';
import '@testing-library/jest-dom';
import { render, fireEvent } from 'react-testing-library';
위의 import 구문을 아래 구문으로 대체
import { render, fireEvent } from "@testing-library/react";
setupTests.js에서 import에러가 나는 경우는 어떻게 해결해야 할까요?