React

toast ui RTL

Asset Type
File Type
When to use
Last edited time
2022/05/05 12:56
Created by
Reference

왜 RTL인가?

React 컴포넌트 테스트용 라이브러리다.
과거에 Enzyme을 사용했을 수 있다. 다른 점은 React 컴포넌트의 인스턴스가 아닌 실제 DOM 노드를 사용한다는 점이다.
웹 브라우저 환경과 유사한 환경에서 테스트 케이스가 실행된다는 것을 의미한다. 비슷할수록 신뢰할 수 있다.
또 다른 이유는 사용자가 앱과 인터랙션하는 방식과 유사해야 한다는 기본적인 라이브러리 철학 때문이다.
사용자는 state, props와 인터랙션하는 것을 모른다.
함수 컴포넌트에서 훅을 사용하는지, 클래스 컴포넌트와 함께 고차 컴포넌트를 사용하는지 신경 쓰지 않는다.
그저 인터페이스를 보고 인터랙션한다.
올바른 props, state인지 테스트하는 것 대신 사용자가 보고 수행하는 작업을 테스트하도록 설계되었다. 따라서 접근 가능한 UI를 구축하고 HTML을 구성할 때 모범 사례를 준수할 수 있다.

철학 적용하기

테스트케이스 1

앱이 로딩되면 사용자가 가장 먼저 보게 되는 것은 무엇인가?
이름과 성을 입력하는 제목과 21세 이상인지 묻는 체크 박스와 취소/제출 버튼이다.
사용자가 처음에 보게 될 것을 테스트하는 것을 좋아한다.

테스트케이스 2

다음 인터랙션은 Form 작성을 하는 것이다. 따라서 사용자가 Form을 작성하기 시작하면 “21세 이상입니까?”라는 메시지와 체크박스가 표시된다.
사용자가 체크박스를 클릭하면 좋아하는 음료를 입력할 수 있도록 조건부로 또 다른 입력이 나타난다.
이건 테스트해야 하는 별도의 코드 분기를 나타낸다.
이 테스트가 어떻게 useState의 사용을 직접적으로 테스트하지 않는지 눈여겨보라.
우리는 내부 state가 true나 false로 바뀌는 것이 아닌 사용자가 올바른 정보를 보는지 테스트하려는 것이다.
useReducer나 다른 상태 관리 솔루션을 사용하도록 리팩터링해도 테스트는 변경할 필요가 없다.

테스트케이스 3, 4

사용자가 마지막으로 할 수 있는 건 취소/제출이다. 부모 컴포넌트에서 콜백함수를 넘겨주는 방식이다. 이전 테스트 케이스와 약간 다르다.
사용자가 보는 것을 테스트하는 것이 아니라 특정 함수를 호출해 사용자의 작업에 내부적으로 올바르게 반응하는지 테스트한다.

선언적 프로그래밍을 사용하여 테스트 작성하기

it("뭔가 수행한다.", async () => { const onSubmit = jest.fn(); const onCancel = jest.fn(); const result = render(<ComplexForm onSubmit={onSubmit} onCancel={onCancel} />); expect(result.getByLabelText('First Name')).toBeInTheDocument(); expect(result.getByLabelText('Last Name')).toBeInTheDocument(); await act(async () => { userEvent.click(result.getByLabelText('Over 21?')); }); expect(result.getByLabelText('Favorite Drink?')).toBeInTheDocument(); });
TypeScript
복사
간단한 컴포넌트, 1~2개 테스트면 괜찮다.
이런 테스트는 파일이 크고 빠르게 이해하기 어렵다. 진행상황을 이해하기 위해 코드 라인을 주의깊게 읽어야 한다.
하는 일을 보여주는 테스트 대신 사용자의 의도를 설명하는 테스트를 작성하자.
it("뭔가 수행한다.", async () => { const { FirstNameInput, LastNameInput, clickIsOver21, FavoriteDrinkInput } = renderComplexForm(); // 이름 입력, 성 입력, 21이상인지 체크박스, 즐겨 찾는 음료 입력 expect(FirstNameInput()).toBeInTheDocument(); expect(LastNameInput()).toBeInTheDocument(); // 입력 2개 잘 들어오나? await clickIsOver21(); // 클릭을 비동기로 기다림 expect(FavoriteDrinkInput()).toBeInTheDocument(); // 입력 잘 들어오나? });
TypeScript
복사
함수만 읽어도 현재 상황을 즉시 파악할 수 있다.
이름과 성 입력이 doc에 있는지 확인한다. 그 다음 ‘21세 이상입니까?’ 체크박스를 클릭한 다음 즐겨 찾는 음료 입력이 doc에 있는지 확인한다.
읽기 쉽고 renderComplexForm 함수에서 내보낸 테스트 헬퍼를 다른 테스트 케이스에서 재사용할 수 있다.
코드 중복을 줄이고 가독성을 크게 높여야 한다. 기능을 추가하고 테스트를 업데이트할 때 훨씬 쉽다.
대규모 프로젝트에서 매우 원활하게 확장하고 복잡한 컴포넌트를 쉽게 테스트할 수 있다.

ComplexForm 컴포넌트에 대한 테스트 작성

import React from "react"; import userEvent from "@testing-library/user-event"; import { act, render } from "@testing-library/react"; import ComplexForm, { ComplexFormProps } from "./ComplexForm"; /** * 이건 모든 테스트에서 호출되는 테스트 설정이다. * * 테스트 설정 함수를 만들면 테스트 케이스에 대해 작성해야 하는 반복 코드의 양이 * 줄어들고 테스트중인 컴포넌트와 상호작용하기 위한 선언적 테스트 헬퍼를 설정할 수 있다. */ function renderComplexForm(props?: Partial<ComplexFormProps>) { /* 제출과 취소 버튼을 위한 mock 콜백 함수를 설정한다. */ const onSubmit = jest.fn(); const onCancel = jest.fn(); /* React Testing Library를 사용해 컴포넌트를 렌더링한다. */ const result = render(<ComplexForm onSubmit={onSubmit} onCancel={onCancel} {...props} />); /* 다음 7개의 함수는 컴포넌트에서 공통 DOM 요소를 가져오기 위한 헬퍼 함수이다. */ const Heading = () => result.getByText("Welcome, Zerry"); const FirstNameInput = () => result.getByLabelText("First Name"); const LastNameInput = () => result.getByLabelText("Last Name"); const IsOver21Input = () => result.getByLabelText("Are you at least 21 years old?"); const FavoriteDrinkInput = () => result.queryByLabelText("What's your favorite drink?"); const CancelButton = () => result.getByText("Cancel"); const SubmitButton = () => result.getByText("Apply"); /* 다음 6개의 함수는 DOM 요소와 상호작용하기 위한 헬퍼 함수이다. */ function changeFirstName(name: string) { userEvent.type(FirstNameInput(), name); } function changeLastName(name: string) { userEvent.type(LastNameInput(), name); } function changeFavoriteDrinkInput(name: string) { userEvent.type(FavoriteDrinkInput() as HTMLElement, name); } async function clickIsOver21() { await act(async () => { userEvent.click(IsOver21Input()); }); } function clickSubmit() { userEvent.click(SubmitButton()); } function clickCancel() { userEvent.click(CancelButton()); } /* 마지막으로 이 유틸리티 렌더 함수에서 모든 함수와 상수를 내보낸다. 이를 통해 모든 테스트 케이스에서 필요한 것을 얻을 수 있다. */ return { result, onSubmit, changeFirstName, changeLastName, clickIsOver21, clickSubmit, clickCancel, FirstNameInput, LastNameInput, IsOver21Input, SubmitButton, CancelButton, Heading, FavoriteDrinkInput, changeFavoriteDrinkInput, onCancel, }; } describe("<ComplexForm />", () => { it("기본 필드를 렌더링해야 한다.", async () => { const { FirstNameInput, LastNameInput, IsOver21Input, SubmitButton, Heading, FavoriteDrinkInput, CancelButton } = renderComplexForm(); // 헤더 expect(Heading()).toBeInTheDocument(); // 입력 expect(FirstNameInput()).toBeInTheDocument(); expect(LastNameInput()).toBeInTheDocument(); expect(IsOver21Input()).toBeInTheDocument(); expect(FavoriteDrinkInput()).not.toBeInTheDocument(); // 버튼들 expect(CancelButton()).toBeInTheDocument(); expect(SubmitButton()).toBeInTheDocument(); }); it("21세 이상 체크 여부에 따라 좋아하는 음료 입력을 토글해야한다.", async () => { const { clickIsOver21, FavoriteDrinkInput } = renderComplexForm(); expect(FavoriteDrinkInput()).not.toBeInTheDocument(); await clickIsOver21(); expect(FavoriteDrinkInput()).toBeInTheDocument(); }); it("취소 버튼이 클릭되면 onCancel 함수가 호출되야 한다.", async () => { const { clickCancel, onCancel } = renderComplexForm(); clickCancel(); expect(onCancel).toHaveBeenCalled(); }); it("form 값으로 onSubmit을 호출해야 한다.", async () => { const { changeFirstName, changeLastName, clickIsOver21, changeFavoriteDrinkInput, clickSubmit, onSubmit } = renderComplexForm(); changeFirstName('Zerry'); changeLastName('Hogan'); await clickIsOver21(); changeFavoriteDrinkInput('Bourbon'); clickSubmit(); expect(onSubmit).toHaveBeenCalledWith({ first_name: 'Zerry', last_name: 'Hogan', is_over_21: true, favorite_drink: 'Bourbon', }); }); });
TypeScript
복사
나눠서 설명하겠다.
1.
먼저 컴포넌트에 대한 렌더링 함수를 만든다. render 함수는 React Testing Library를 사용해 컴포넌트를 렌더링하고 테스트 케이스를 위한 헬퍼 함수를 내보내는 역할을 한다. 렌더링 기능을 위한 별도의 파일을 만들어 테스트로 가져올 수도 있다.
2.
각 테스트 케이스는 renderComplexForm을 호출해 특정 테스트 케이스에 필요한 유틸리티 함수를 가져온다.
3.
입력 값을 변경하기 위해 changeFirstname 테스트 헬퍼 함수를 만들었다. 사용자가 상호작용하는 방식을 시뮬레이션하고 테스트에서 어떤 일이 일어나는지 명백히 보여준다.
4.
renderComplexForm 함수는 props 인수를 받는다. 컴포넌트가 컴포넌트의 UI 또는 사용자가 보는 것을 변경하는 props를 받는 경우가 많다. 각 테스트 케이스가 props를 넘기도록 허용해 서로 다른 상호작용을 테스트할 수도 있다.
5.
onSubmit과 onCancel props를 위해 jest mock 함수를 사용하고 있다. jest mock 함수는 함수가 호출되었는지, 몇 번이나 호출되었는지, 그리고 어떤 인수로 호출되었는지 테스트하는 데 유용하다. 마지막 두 테스트 케이스에서 버튼 클릭 시 적절한 콜백 함수를 호출했는지 테스트하기 위해 jest mock 함수를 사용했다.
결과적으로 더욱 읽기 쉽고 확장 가능하며 오래 지속될 수 있는 테스트를 만들었다고 믿는다. 이 테스트로 돌아와서 입력을 추가하고 테스트 코드를 구문 분석하여 새 테스트 적용 범위를 추가할 위치를 알 필요 없이 몇 분 안에 테스트를 업데이트할 수 있다.