React 통합 테스팅으로 코드 유지보수성 향상
게시 됨: 2022-03-11통합 테스트는 비용과 테스트 가치 사이의 최적의 지점입니다. 구성 요소 단위 테스트 대신 또는 추가로 react-testing-library를 사용하여 React 앱에 대한 통합 테스트를 작성하면 개발 속도를 저하시키지 않으면서 코드 유지 관리성을 높일 수 있습니다.
진행하기 전에 먼저 시작하고 싶다면 여기에서 React 앱 통합 테스트에 react-testing-library를 사용하는 방법의 예를 볼 수 있습니다.
통합 테스트에 투자하는 이유는 무엇입니까?
"통합 테스트는 자신감과 속도/비용 사이의 균형에서 훌륭한 균형을 유지합니다. 그렇기 때문에 노력의 대부분(전부는 아님)을 거기에 투자하는 것이 좋습니다."
– 쓰기 테스트의 Kent C. Dodds. 너무 많지 않습니다. 대부분 통합.
React "효소"를 테스트하기 위해 널리 사용되는 라이브러리를 사용하여 React 구성 요소에 대한 단위 테스트를 작성하는 것이 일반적인 관행입니다. 특히, "얕은" 방법입니다. 이 접근 방식을 통해 앱의 나머지 부분과 격리된 구성 요소를 테스트할 수 있습니다. 그러나 React 앱을 작성하는 것은 구성 요소를 구성하는 것이므로 단위 테스트만으로는 앱에 버그가 없는지 확인할 수 없습니다.
예를 들어 구성 요소의 허용되는 props를 변경하고 관련 단위 테스트를 업데이트하면 모든 테스트를 통과할 수 있지만 다른 구성 요소가 적절하게 업데이트되지 않은 경우 앱이 여전히 손상될 수 있습니다.
통합 테스트는 구성 요소의 구성이 원하는 UX를 생성하도록 보장하므로 React 앱을 변경하는 동안 마음의 평화를 유지하는 데 도움이 될 수 있습니다.
React 앱 통합 테스트 요구 사항
다음은 통합 테스트를 작성할 때 React 개발자가 하고자 하는 몇 가지입니다.
- 사용자 관점에서 애플리케이션 사용 사례를 테스트합니다. 사용자는 웹 페이지의 정보에 액세스하고 사용 가능한 컨트롤과 상호 작용합니다.
- API 가용성 및 테스트 통과/실패 상태에 의존하지 않는 모의 API 호출.
- 단순히 테스트 환경에 존재하지 않기 때문에 모의 브라우저 API(예: 로컬 스토리지).
- React DOM 상태(브라우저 DOM 또는 기본 모바일 환경)에 대한 어설션.
이제 React 앱 통합 테스트를 작성할 때 피해야 할 몇 가지 사항이 있습니다.
- 구현 세부 정보를 테스트합니다. 구현 변경은 실제로 버그가 발생한 경우에만 테스트를 중단해야 합니다.
- 너무 많이 조롱하십시오. 우리는 앱의 모든 부분이 어떻게 함께 작동하는지 테스트하고 싶습니다.
- 얕은 렌더링. 가장 작은 구성 요소까지 앱의 모든 구성 요소 구성을 테스트하려고 합니다.
React-testing-library를 선택하는 이유는 무엇입니까?
앞서 언급한 요구 사항은 react-testing-library를 훌륭한 선택으로 만듭니다. 주요 지침 원칙은 실제 사람이 사용하는 방식과 유사한 방식으로 React 구성 요소를 테스트할 수 있도록 하는 것입니다.
이 라이브러리는 선택적 컴패니언 라이브러리와 함께 DOM과 상호 작용하는 테스트를 작성하고 해당 상태에 대해 주장할 수 있습니다.
샘플 앱 설정
샘플 통합 테스트를 작성할 앱은 간단한 시나리오를 구현합니다.
- 사용자가 GitHub 사용자 이름을 입력합니다.
- 앱은 입력한 사용자 이름과 연결된 공개 저장소 목록을 표시합니다.
위의 기능이 구현되는 방식은 통합 테스트 관점에서 볼 때 관련이 없습니다. 그러나 실제 애플리케이션에 가깝게 유지하기 위해 앱은 일반적인 React 패턴을 따르므로 앱은 다음과 같습니다.
- 단일 페이지 앱(SPA)입니다.
- API 요청을 합니다.
- 글로벌 상태 관리가 있습니다.
- 국제화를 지원합니다.
- React 구성 요소 라이브러리를 활용합니다.
앱 구현을 위한 소스 코드는 여기에서 찾을 수 있습니다.
통합 테스트 작성
종속성 설치
원사 포함:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
또는 npm 사용:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
통합 테스트 스위트 파일 생성
애플리케이션의 ./test
폴더에 viewGitHubRepositoriesByUsername.spec.js
파일을 생성합니다. Jest가 자동으로 선택합니다.
테스트 파일에서 종속성 가져오기
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
테스트 스위트 설정하기
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
메모:
- 모든 테스트 전에 GitHub API를 모의하여 특정 사용자 이름으로 호출할 때 리포지토리 목록을 반환합니다.
- 각 테스트가 끝나면 테스트 React DOM을 정리하여 각 테스트가 깨끗한 지점에서 시작되도록 합니다.
-
describe
블록은 통합 테스트 사용 사례와 흐름 변형을 지정합니다. - 테스트 중인 흐름 변형은 다음과 같습니다.
- 사용자는 연결된 공개 GitHub 리포지토리가 있는 유효한 사용자 이름을 입력합니다.
- 사용자가 연결된 공용 GitHub 리포지토리가 없는 유효한 사용자 이름을 입력합니다.
- 사용자가 GitHub에 없는 사용자 이름을 입력합니다.
- 테스트 중인 사용 사례에 비동기 단계가 있으므로 비동기 콜백 사용
it
차단합니다.
첫 번째 흐름 테스트 작성
먼저 앱을 렌더링해야 합니다.
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
@testing-library/react
모듈에서 가져온 render
메서드는 테스트 React DOM에서 앱을 렌더링하고 렌더링된 앱 컨테이너에 바인딩된 DOM 쿼리를 반환합니다. 이러한 쿼리는 상호 작용하고 어설션할 DOM 요소를 찾는 데 사용됩니다.
이제 테스트 중인 흐름의 첫 번째 단계로 사용자에게 사용자 이름 필드가 표시되고 여기에 사용자 이름 문자열을 입력합니다.
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
가져온 @testing-library/user-event
모듈의 userEvent
도우미에는 텍스트 필드에 텍스트를 입력할 때 사용자의 동작을 모방하는 type
메서드가 있습니다. 입력을 받아들이는 DOM 요소와 사용자가 입력하는 문자열의 두 가지 매개변수를 받습니다.
사용자는 일반적으로 연관된 텍스트로 DOM 요소를 찾습니다. 입력의 경우 레이블 텍스트 또는 자리 표시자 텍스트입니다. 이전에 render
에서 반환된 getByPlaceholderText
쿼리 메서드를 사용하면 자리 표시자 텍스트로 DOM 요소를 찾을 수 있습니다.

텍스트 자체는 종종 변경될 수 있으므로 실제 현지화 값에 의존하지 않고 현지화 항목 키를 값으로 반환하도록 현지화 모듈을 구성하는 것이 가장 좋습니다.
예를 들어 "en-US" 현지화가 일반적으로 userSelection.usernamePlaceholder
키에 대한 값으로 Enter GitHub username
을 반환할 때 테스트에서 userSelection.usernamePlaceholder
반환하기를 원합니다.
사용자가 필드에 텍스트를 입력하면 업데이트된 텍스트 필드 값이 표시되어야 합니다.
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
다음 흐름에서 사용자는 제출 버튼을 클릭하고 리포지토리 목록을 보기를 기대합니다.
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
userEvent.click
메소드는 사용자가 DOM 요소를 클릭하는 것을 모방하는 반면 getByText
쿼리는 포함된 텍스트로 DOM 요소를 찾습니다. closest
수정자는 올바른 종류의 요소를 선택하도록 합니다.
참고: 통합 테스트에서 단계는 종종 act
과 assert
역할을 모두 수행합니다. 예를 들어 사용자가 버튼을 클릭하여 클릭할 수 있다고 주장합니다.
이전 단계에서 우리는 사용자가 앱의 리포지토리 목록 섹션을 본다고 주장했습니다. 이제 GitHub에서 리포지토리 목록을 가져오는 데 시간이 걸릴 수 있으므로 가져오기가 진행 중이라는 표시가 사용자에게 표시된다고 주장해야 합니다. 또한 리포지토리 목록을 가져오는 동안 입력한 사용자 이름과 연결된 리포지토리가 없다고 앱이 사용자에게 알리지 않도록 하고 싶습니다.
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
getBy
쿼리 접두사는 DOM 요소를 찾을 수 있다고 주장하는 데 사용되고 queryBy
쿼리 접두사는 반대 주장에 유용합니다. 또한 queryBy
는 요소가 발견되지 않으면 오류를 반환하지 않습니다.
다음으로, 결국 앱이 리포지토리 가져오기를 완료하고 사용자에게 표시하는지 확인하고 싶습니다.
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
waitForElement
비동기 메소드는 메소드 매개변수 true로 제공된 어설션을 렌더링할 DOM 업데이트를 기다리는 데 사용됩니다. 이 경우 앱이 조롱된 GitHub API에서 반환된 모든 리포지토리의 이름과 설명을 표시한다고 주장합니다.
마지막으로 앱은 리포지토리를 가져오는 중이라는 표시를 더 이상 표시하지 않아야 하고 오류 메시지를 표시하지 않아야 합니다.
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
결과 React 통합 테스트는 다음과 같습니다.
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
대체 흐름 테스트
사용자가 연결된 공용 리포지토리 없이 GitHub 사용자 이름을 입력하면 앱에 적절한 메시지가 표시됩니다.
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
사용자가 존재하지 않는 GitHub 사용자 이름을 입력하면 앱에 오류 메시지가 표시됩니다.
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
React 통합 테스트가 어려운 이유
통합 테스트는 실제로 React 애플리케이션을 위한 최적의 장소를 제공합니다. 이러한 테스트는 버그를 포착하고 TDD 접근 방식을 사용하는 데 도움이 되는 동시에 구현이 변경될 때 유지 관리가 필요하지 않습니다.
이 기사에 소개된 React-testing-library는 사용자가 하는 것처럼 앱과 상호 작용하고 사용자의 관점에서 앱 상태와 동작을 검증할 수 있도록 해주기 때문에 React 통합 테스트를 작성하기 위한 훌륭한 도구입니다.
여기에 제공된 예제가 신규 및 기존 React 프로젝트에 대한 통합 테스트 작성을 시작하는 데 도움이 되기를 바랍니다. 앱 구현을 포함하는 전체 샘플 코드는 내 GitHub에서 찾을 수 있습니다.