리액트에서 테스트를 위해서 많이 사용하는 도구 중 하나인 React Testing Library
에 대한 포스트입니다.
테스트에 사용되는 라이브러리들이 많으니 각자 본인에게 맞는 것을 활용해 공부하며 적용해 보는 것이 좋을 것 같습니다.
아래에 리액트 공식 사이트에서 사용되는 테스트 유틸 및 라이브러리 예시를 확인해보세요.
Enzyme
: React를 위한 JavaScript 테스트 유틸.
Jest
: React를 포함한 JavaScript 테스트 프레임워크.
react-testing-library
: 가벼운 React DOM 테스트 유틸.
React-unit
: React를 위한 가벼운 단위테스트 라이브러리.
Skin-deep
: 얕은 렌더링을 지원하는 React 테스트 유틸.
Unexpected-react
: React 컴포넌트와 이벤트를 발생시켜주는 플러그인.
설치
yarn add @testing-library/react @testing-library/jest-dom
npm install --save react-testing-library @testing-library/jest-dom
npm 또는 yarn을 이용해 react-testing-library
를 설치한다. react-testing-library
가 @testing-library/react
로 변경되었으니 주의!
jest-dom
도 @testing-library/jest-dom
으로 변경됐다고 한다.
react-testing-library는 '사용자 관점'에서 테스트를 진행한다. 보통 Enzyme를 이용한 테스트 방식은 상태값, 상태 변수에 대해 테스트를 한다. 하지만 전자는 상태 관리는 컴포넌트의 구현 세부사항일 뿐이다. 즉, 상태 변수가 언제든 다른 컴포넌트로 옮겨지거나, react에서 vue.js로 바뀌더라도 테스트에는 문제가 없어야한다.
다양한 쿼리
- getBy* 쿼리 (ex.
getByTestId
, getByText
, getByRole
): 이 함수들은 동기적(synchronous)이며 그 요소가 현재 DOM 안에 있는지 확인한다. 즉, 현재상태만 확인한다. 그렇지 않으면 에러를 발생시킨다.
- findBy* 쿼리 (ex.
findByText
): 이 함수들은 비동기적(asynchronous)이다. 그 요소를 찾을 때까지 일정 시간(기본 5초)을 기다린다. 만약 그 시간이 지난 후에도 요소를 찾을 수 없으면 에러를 발생시킨다.
- queryBy* 쿼리: 이 함수들은 getBy* 처럼 동기적이다. 하지만 요소를 찾을 수 없어도 에러를 발생시키지 않는다. 단지
null
값을 리턴한다.
테스트 디버깅하기
render(<SomeComponent />);
screen.debug(); //원하는 실행 지점에서 DOM트리를 console에 출력해, 디버깅할 수 있다.
screen.debug()
는 브라우저의 개발 도구처럼 편리하고 상호작용적이진 않지만, 테스트 환경에서 어떤 일이 일어나고 있는지 확실히 알 수 있도록 도와준다.
const link = screen.getByRole('link', { name: /how it works/i });
screen.debug(link);
위와 같이 debug
함수에 파라미터를 전달하면 해당 요소만을 콘솔에 출력해준다.
Mocking
jest.fn
: Mock a function
jest.mock
: Mock a module → 자동적으로 모듈의 모든 함수를 mocking 해준다.
jest.spyOn
: Spy or mock a function → 마찬가지로 모든 함수를 mocking 해주면서, 원래의 함수를 다시 복원할 수도 있다.
가장 기본적인 사용 방식은 함수를 mock 함수로 재할당하는 것이다. 재할당 된 함수가 쓰이는 어디서든지 mock 함수가 원래의 함수 대신 호출 될 것이다.
예시
- jest.fn() mocking
// app.js
import * as math from './math.js';
export const doAdd = (a, b) => math.add(a, b);
export const doSubtract = (a, b) => math.subtract(a, b);
export const doMultiply = (a, b) => math.multiply(a, b);
export const doDivide = (a, b) => math.divide(a, b);
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => b - a;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => b / a;
// app.test.js
import * as app from "./app";
import * as math from "./math";
math.add = jest.fn();
math.subtract = jest.fn();
test("calls math.add", () => {
app.doAdd(1, 2);
expect(math.add).toHaveBeenCalledWith(1, 2);
});
test("calls math.subtract", () => {
app.doSubtract(1, 2);
expect(math.subtract).toHaveBeenCalledWith(1, 2);
});
- jest.mock() mocking
jest.mock('./math.js');
위의 코드로 mocking하는 것은 본질적으로 아래 코드처럼 하는거랑 같다.
export const add = jest.fn();
export const subtract = jest.fn();
export const multiply = jest.fn();
export const divide = jest.fn();
// app.test.js
import * as app from "./app";
import * as math from "./math";
// Set all module functions to jest.fn
jest.mock("./math.js");
test("calls math.add", () => {
app.doAdd(1, 2);
expect(math.add).toHaveBeenCalledWith(1, 2);
});
test("calls math.subtract", () => {
app.doSubtract(1, 2);
expect(math.subtract).toHaveBeenCalledWith(1, 2);
});