React Testing Library 사용법

React Testing Library(RTL)라는 테스팅 라이브러리가 React 개발자들의 입소문을 타고 점점 인기가 올라가고 있습니다.
이번 포스트에서는 최근 React에서 테스팅 트랜드와 패러다임을 바꾸고 있는 React Testing Library에서 다뤄보도록 하겠습니다.

React Testing Library 소개

React Testing Library는 Behavior Driven Test(행위 주도 테스트) 방법론이 대두 되면서 함께 주목 받기 시작한 테스팅 라이브러리 입니다.
행위 주도 테스트는 기존에 관행이던 Implementation Driven Test(구현 주도 테스트)의 단점을 보완하기 위한 방법론인데요.

Implementation Driven Test에서는 주로 애플리케이션이 어떻게 작동하는지에 대해서 초점을 두어 테스트를 작성합니다.

1
<h2 class="title">제목</h2>

예를 들어, 위와 같은 UI를 테스트한다고 했을 때, <h2>라는 태그가 쓰였고, title 이라는 클래스가 사용되었는지 여부를 테스트합니다.

반면에, Behavior Driven Test에서는 사용자가 애플리케이션을 이용하는 관점에서 사용자의 실제 경험 위주로 테스트를 작성합니다.
사용자가 위 UI를 사용하면서 과연 <h2> 태그를 사용하고, title 이라는 클래스가 사용되었는지 관심이나 있을까요?
사용자 입장에서는 단지 브라우저 화면에 제목 이라는 텍스트가 보일 뿐입니다.
따라서, 사용자에게 어떤 컨텐츠가 현재 보이고, 사용자가 어떤 이벤트를 발생시켰을 때, 그에 따라 화면에 변화가 일어나는지를 테스트합니다.

자 여기서, 만약에 추후 어떤 이유로 인해서 <h2> 태그가 <h3> 태그로 바뀌었을 때는 어떤 일이 날까요?
Implementation Driven Test 방법론으로 작성된 테스트는 깨지지만, Behavior Driven Test 방밥론으로 작성된 테스트는 깨지지 않을 것입니다.
애플리케이션 입장에서 봤을 때는 구현의 디테일이 바뀐 것이지만, 사용자 입장에서는 제목 이라는 텍스트가 화면있다는 사살은 변함이 없기 때문입니다.

Enzyme vs React Testing Library

RTL이 등장하기 전에는 Airbnb에서 만든 Enzyme이라는 테스팅 라이브러리가 많이 사용되고 있어서, 두 라이브러리를 잠깐 비교해보도록 하겠습니다.
Enzyme은 위에서 설명드린 Implementation Driven Test 방법론을 따르는 테스트를 작성하기에 적합합니다.
왜냐하면 Enzyme은 실제 브라우저 DOM이 아닌, React가 만들어내는 가상 DOM을 기준으로 테스트를 작성해야 합니다.
따라서 테스트 대상 React 컴포넌트에 어떤 prop이 넘어가고, 현재 state이 어떻게 되는지에 대해서 검증하기 용이합니다.

반면에, React Testing Library는 Behavior Driven Test 방밥론을 따르는 테스트를 작성하는데 적합합니다.
왜냐하면 React Testing Library는 jsdom이라는 라이브러를 통해 실제 브라우저 DOM을 기준으로 테스트를 작성하게 됩니다.
따라서 어떤 React 컴포넌트를 사용하는지는 의미가 없으며, 결국 사용자 브라우저에서 랜더링하는 실제 HTML 마크업의 모습이 어떤지에 대해서 테스트하기 용이합니다.

개인적으로 두 가지 테스팅 라이브러리 다 써보았는데, 둘 다 잘하는 영역이 있기 때문에 하나가 다른 것보다 항상 났다라고는 말하기 어려운 것 같습니다.
단지, 위에서 설명드린 것 처럼 테스트 패러다임 자체가 Behavior Driven Test 쪽으로 흘러가는 추세이기 때문에,
앞으로 점점 신규 프로젝트에서는 Enzyme이라는 보다는 React Testing Library가 많이 선택될 확률이 높을 것입니다.

React Testing Library 설치

React에서 가장 많이 쓰이는 테스팅 프레임워크인 Jest를 기반으로 React Testing Library 설치하도록 하겠습니다.

먼저, 자신의 React 프로젝트에 아직 Jest가 설치하지 않았다면 먼저 jest 설치합니다.
테스팅 라이브러리이기 때문에 개발 의존성(-D 옵션)으로 설치합니다.
(create-react-app으로 생성된 프로젝트는 기본적으로 jest가 내장되어 있으니 스킵바랍니다.)

1
npm i -D jest

Jest에 대한 자세한 사용법에 대해서는 별도로 포스트를 올려놓았니 참고 바랍니다.

다음, React Testing Library를 설치합니다. 패키지 이름은 @testing-library/react 입니다.
간혹 아직 업데이트가 안 된 튜토리얼의 경우, 패키지 이름이 react-testing-library 안내되고 있지만, 반드시 @testing-library/react를 설치해야합니다.
(패키지 이름이 바꿘 사연은 React Testing Library의 저자인 Kent C. Dodds의 트위트를 참고바랍니다.)

1
npm i -D @testing-library/react

마지막으로 Jest의 DOM에 특화된 matcher 애드온인 jest-dom을 설치합니다.

1
npm i -D jest-dom

jest-dom에서 제공하는 custom matcher 목록은 아래 링크를 참조바랍니다.

React Testing Library 설정

React Testing Library를 사용하려면 두 가지 설정이 필요합니다.
첫 째는 각 테스트가 DOM에 렌더링해놓은 내용들을 테스트가 끝날 때 다음 테스트를 위해서 지워주는 것이고, 두 번째는 jest-dom가 제공하는 matcher를 Jest 테스트 러너에게 인식시키는 것입니다.

아래 두 줄의 코드를 자신의 React 프로젝트의 테스팅 설정 파일에 추가해주면 됩니다.
예를 들어 create-react-app으로 생성된 프로젝트라면 src/setupTests.js 파일에 추가해주면 됩니다.

1
2
import "@testing-library/react/cleanup-after-each";
import "jest-dom/extend-expect";

React Testing Library 주요 API

React Testing Library는 매우 심플하지만 강력한 API를 가지고 있습니다.
크게 DOM에 컴포넌트를 랜더링 해주는 render() 함수와, 특정 이벤트를 발생시켜주는 fireEvent 객체, 그리고 DOM에서 특정 영역을 선택하기 위한 다양한 쿼리 함수들이 존재합니다.

render() 함수는 @testing-library/react 모듈로 부터 바로 임포트가 가능하며, 인자로 랜더링할 React 컴포넌트를 넘김니다.
그리고 render() 함수는 React Testing Library 제공하는 모든 쿼리 함수와 기타 유틸리티 함수 담고 있는 객체를 리턴합니다.
따라서 다음과 같이 자바스크립트의 객체 Destructuring 문법으로 render() 함수가 리턴한 객체로 부터 원하는 쿼리 함수만 얻어올 수 있습니다.

1
2
3
import { render, fireEvent } from "@testing-library/react";

const { getByText, getByLabelText, getByPlaceholderText } = render(<YourComponent />);

쿼리 함수는 getByXxx() 외에도 queryByXxx()findByXxx() 등 다양하게 존재하는데 자세한 내용은 아래 문서를 참고바랍니다.

fireEvent 객체는 쿼리 함수로 선택된 영역을 대상으로 특정 이벤트를 발생시키기 위한 이벤트 함수들을 담고 있는데, 아래 나오는 예제를 통해 어떻게 사용하는지 좀 더 구체적으로 살펴보겠습니다.

정적 컴포넌트 테스팅

자, 이제 본격적으로 React Testing Library를 사용해서 테스트를 작성해보도록 하겠습니다.
아래 코드는 매우 심플한 페이지를 찾을 수 없음 페이지입니다. 내부 상태가 없고 단순히 고정된 텍스트와 이미지로만 구성되어 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";

function NotFound({ path }) {
return (
<>
<h2>Page Not Found</h2>
<p>해당 페이지({path})를 찾을 수 없습니다.</p>
<img
alt="404"
src="https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif"
/>
</>
);
}

먼저 헤더가 랜더링되고 있는지를 검증하는 테스트를 작성해보겠습니다.

일단 테스트 파일에 React를 임포트 후에, @testing-library/react 모듈로 부터 render 함수를 임포트합니다.
그리고 테스트 대상인 <NotFound /> 컴포넌트도 임포트합니다.

1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";

describe("<NotFound />", () => {
it("renders header", () => {
const { getByText } = render(<NotFound path="/abc" />);
const header = getByText("Page Not Found");
expect(header).toBeInTheDocument();
});
});

그 다음, <NotFound /> 컴포넌트를 render 함수의 인자로 넘긴 후에, 리턴 객체로 부터 getByText()라는 함수를 얻습니다.
그리고, getByText()에 화면에서 검색할 텍스트인 "Page Not Found"를 인자로 넘긴 후에, 해당 텍스트를 담고 있는 <h2/> 엘리먼트를 얻습니다.
마지막으로 jest-domtoBeInTheDocument() matcher 함수를 이용해서 해당 <h2/> 엘리먼트가 화면에 존재하는 검증합니다.

동일한 방식으로 본문이 제대로 랜더링되고 있는지 검증해볼 수 있습니다.
getByText() 쿼리 함수는 문자열 뿐만 아니라 정규식도 인자로 받을 수 있습니다.
jest-domtoHaveTextContent() matcher 함수를 이용해서 <p/> 엘리먼트 속의 텍스트가 예상과 일치하는지 검증합니다.

1
2
3
4
5
6
7
it("renders paragraph", () => {
const { getByText } = render(<NotFound path="/abc" />);
const paragraph = getByText(/^해당 페이지/);
expect(paragraph).toHaveTextContent(
"해당 페이지(/abc)를 찾을 수 없습니다."
);
});

이미지 같은 경우, <img/> 태그는 내부에 텍스트가 없기 때문에, 대신 alt 속성값을 이용하는 getAltText() 쿼리 함수를 사용합니다.
jest-domtoHaveAttribute() matcher 함수를 이용해서 <img/> 엘리먼트의 src 속성값이 정확한지 검증합니다.

1
2
3
4
5
6
7
8
it("renders image", () => {
const { getByAltText } = render(<NotFound path="/abc" />);
const image = getByAltText("404");
expect(image).toHaveAttribute(
"src",
"https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif"
);
});

동적 컴포넌트 테스팅

이번에는 내부 상태에 따라 UI에 변화가 생길 수 있는 좀 더 복잡한 컴포넌트에 대한 테스트를 작성해보겠습니다.
아래 코드는 이메일과 비밀번호 입력란과 버튼으로 구성된 간단한 로그인 폼입니다.

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
31
32
import React, { useState } from "react";

function LoginForm({ onSubmit }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

return (
<>
<h2>Login</h2>
<form onSubmit={onSubmit}>
<label>
이메일
<input
type="email"
placeholder="user@test.com"
value={email}
onChange={({ target: { value } }) => setEmail(value)}
/>
</label>
<label>
비밀번호
<input
type="password"
value={password}
onChange={({ target: { value } }) => setPassword(value)}
/>
</label>
<button disabled={!email || !password}>로그인</button>
</form>
</>
);
}

먼저 비활성 되어 있던 로그인 버튼이 이메일과 비밀번호가 입력된 후에 활성화되는지에 대한 테스트를 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import LoginForm from "./LoginForm";

describe("<LoginForm />", () => {
it("enables button when both email and password are entered", () => {
const { getByText, getByLabelText } = render(
<LoginForm onSubmit={() => null} />
);

const button = getByText("로그인");
const email = getByLabelText("이메일");
const password = getByLabelText("비밀번호");

expect(button).toBeDisabled();

fireEvent.change(email, { target: { value: "user@test.com" } });
fireEvent.change(password, { target: { value: "Test1234" } });

expect(button).toBeEnabled();
});
});

로그인 버튼의 경우에는 getByText() 쿼리 함수를 통해 선택하고, 이메일과 비밀번호 입력칸은 getByLabelText() 쿼리 함수로 선택하였습니다.
그리고 jest-domtoBeDisabled()toBeEnabled() matcher 함수를 통해서 로그인 버튼의 활성화 여부를 이벤트 발생 전후로 검증합니다.
두 개의 입력칸에 change 이벤트를 발생시키기 위해서 fireEvent.change() 함수를 사용하였습니다.

마지막으로 로그인 버튼을 클릭하였을 때, prop으로 넘긴 onSubmit라는 함수가 호출되는지 여부를 검증합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it("submits form when buttion is clicked", () => {
const obSubmit = jest.fn();
const { getByText, getByLabelText } = render(
<LoginForm onSubmit={obSubmit} />
);

const button = getByText("로그인");
const email = getByLabelText("이메일");
const password = getByLabelText("비밀번호");

fireEvent.change(email, { target: { value: "user@test.com" } });
fireEvent.change(password, { target: { value: "Test1234" } });

fireEvent.click(button);

expect(obSubmit).toHaveBeenCalledTimes(1);
});

이번에는 로그인 버튼에 click 이벤트를 발생시키기 위해서 fireEvent.click() 함수를 사용하였습니다.

jest.fn()이 생소한 분들은 Jest Mock에 대한 포스트를 참고바랍니다.

마치면서

이상으로 React Testing Library를 이용해서 React 컴포넌트에 대한 테스트 코드를 작성하는 방법에 대해서 알아보았습니다.
Jest 테스팅 프레임워크에 대한 선수 지식이 없으시거나, React 코드에 대한 테스팅 경험이 없으신 분들을 따라오시기 어려우셨을 수도 있을 것 같습니다.
전체 코드는 아래 올려두었으니 코드를 처음부터 끝까지 한번 훑어보시는 것도 도움이 되실 것 같습니다.

공유하기