1. TDD(Test Driven Development) : 테스트 주도 개발
React Testing Library 를 정리하기에 앞서 TDD : 테스트 주도 개발에 대해 먼저 얘기할 필요가 있을 것 같다.
TDD란 간단하게 말해서 테스트 코드를 먼저 작성하고 그 테스트를 통과하기 위한 코드를 구현하는 형태의 개발방법론이다.
- TDD 의 개발 절차
TDD의 개발 절차는 실패 -> 성공 -> 리팩토링 3가지로 구성되어 있다. 실패하는 작은 단위의 테스트부터 작성을 하고 해당 테스트를 통과하기 위한 코드를 작성한 뒤 테스트 성공 이후에는 해당 코드에 중복되는 코드는 없는지, 더 개선할 수 있다면 리팩토링의 과정을 거친다.
React 에서는 테스트를 위해 React Testing Library 와 Jest 가 필요하다.
2. React Testing Library
React Testing Library 란?
간단하게 말해 React Component 를 테스트하기 위해 가상 돔을 제공해주는 도구이다.
create-react-app 으로 프로젝트를 만들게 되면 TDD를 위한 기본설정이 되어 있어서 React Testing Library 도 이미 설치가 되어있다.
npm install --save-dev @testing-library/react
하지만 그렇지 않은 경우에는 터미널에서 프로젝트 경로로 들어가서 위와 같이 npm을 통해 설치가 가능하다.
테스트를 진행하려면 npm run test 명령어를 실행하고 a 를 입력하면 모든 테스트를 진행, q를 입력하면 테스트가 종료된다.
cra(create-react-app)으로 프로젝트 설치시 App.test.js가 기본적으로 만들어지고 테스트 진행시 해당파일의 테스트 케이스를 테스트한다. Jest는 {filename}.test.js 또는 {filename}.spec.js 이거나 폴더 이름이 tests인 하위파일들 찾아 테스트를 진행하니 이후 테스트 파일을 만들때 참고.
App.test.js를 열어보면
위와 같이 테스트 케이스가 하나 있는데 test 안에 해당 테스트 케이스의 이름(간략한 description)과 테스트 진행 코드를 실행할 함수가 들어가있다. 함수 안을 보면 render 함수가 있고 인자로 컴포넌트가 들어가있는데 render 함수는 가상 DOM에 해당 React 컴포넌트를 렌더링하는 함수이다.
리턴 값은 React Testing Library 에서 제공하는 쿼리함수와 기타 유틸리티 함수를 담고 있는 객체를 리턴한다. 위와 같이 비구조화 할당을 이용하여 쿼리함수 사용이 가능하다. 하지만 소스코드가 많아지면 사용해야 할 쿼리가 많아지고 복잡해질수 있기 때문에 screen 객체 사용을 권장한다.
쿼리함수란 ?
쿼리는 페이지에서 요소를 찾기위해 테스트 라이브러리가 제공하는 도구이다. 여러 유형의 쿼리(get, find, query)가 있는데 이들 간의 차이점은 요소가 발견되지 않을 경우 get은 오류를 발생, query는 null을 반환, find는 promise를 반환한다.
자세한 설명은 공식 레퍼런스 문서(https://testing-library.com/docs/queries/about/)에 잘 정리가 되어 있으니 참고.
3. Jest
Jest 란 ?
FaceBook에서 만들어진 테스팅 프레임 워크이다. Test Case 를 만들면 해당 테스트를 진행하여 실패인지 성공인지 판별해주는 역할을 한다. Jest 도 RTL(React Testing Library)과 마찬가지로 cra(create react app)를 통해 프로젝트를 만들었을시에는 기본 설정이 되어 있지만 만약 그렇지 않다면 아래와 같이 npm 을 통해 설치가 가능하다.
npm install jest --save-dev
테스트 파일 기본 구조
기본 구조를 보면 describe로 여러 관련있는 테스트들을 하나의 그룹으로 묶어주고 describe안에 test(it)는 개별적인 테스트를 수행 하는 곳이다. test(it)안에 expect 함수는 테스트를 진행할 때마다 사용되며 matcher와 함께 사용된다. 아래의 간단한 예시를 보자.
import React, { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const [disabled, setDisabled] = useState(false);
return (
<div className='App'>
<header className='App-header'>
<h3 data-testid='counter'>{count}</h3>
<button
id='minus-button'
data-testid='minus-button'
disabled={disabled}
onClick={() => setCount((prev) => prev - 1)}>
-
</button>
<button
id='plus-button'
data-testid='plus-button'
disabled={disabled}
onClick={() => setCount((prev) => prev + 1)}>
+
</button>
<div>
<button
style={{ backgroundColor: 'blue' }}
data-testid='on/off-button'
onClick={() => setDisabled((prev) => !prev)}>
on/off
</button>
</div>
</header>
</div>
);
}
export default App;
+ , - 버튼을 클릭하여 count의 상태 값을 증감하고 on/off 버튼으로 +, - 버튼을 활성/비활성하는 간단한 예시이다. cra를 사용하여 진행하였고 해당 화면에 대한 테스트 코드(App.test.js에서 진행)는 아래와 같다.
import { fireEvent, render, screen } from '@testing-library/react';
import App from './App';
describe('Button Counter Tests', () => {
test('the counter starts at 0', () => {
render(<App />);
// counter state 의 초기값이 0인지 확인
const counterElement = screen.getByTestId('counter');
expect(counterElement).toHaveTextContent(0);
});
test('minus button has correct text', () => {
render(<App />);
//마이너스 버튼에 '-' 텍스트가 올바르게 있는지 확인
const buttonElement = screen.getByTestId('minus-button');
expect(buttonElement).toHaveTextContent('-');
});
test('plus button has correct text', () => {
render(<App />);
//마이너스 버튼에 '+' 텍스트가 올바르게 있는지 확인
const buttonElement = screen.getByTestId('plus-button');
expect(buttonElement).toHaveTextContent('+');
});
test('When the + button is pressed, the counter changes to + 1', () => {
render(<App />);
// + 버튼을 클릭하는 이벤트 발생
const buttonElement = screen.getByTestId('plus-button');
fireEvent.click(buttonElement);
// + 버튼을 클릭할 시 counter state가 1로 바뀌었는지 확인
const counterElement = screen.getByTestId('counter');
expect(counterElement).toHaveTextContent('1');
});
test('When the - button is pressed, the counter changes to - 1', () => {
render(<App />);
// - 버튼을 클릭하는 이벤트 발생
const buttonElement = screen.getByTestId('minus-button');
fireEvent.click(buttonElement);
// - 버튼을 클릭할 시 counter state가 -1로 바뀌었는지 확인
const counterElement = screen.getByTestId('counter');
expect(counterElement).toHaveTextContent('-1');
});
test('on/off button has blue color', () => {
render(<App />);
// on/off-button의 background컬러가 'blue'인지 확인
const buttonElement = screen.getByTestId('on/off-button');
expect(buttonElement).toHaveStyle({ backgroundColor: 'blue' });
});
test('prevent the -, + button from being pressed when the on/off button is clicked', () => {
render(<App />);
// on/off-button 클릭하는 이벤트 발생
const onOffButtonElement = screen.getByTestId('on/off-button');
fireEvent.click(onOffButtonElement);
// on/off-button을 클릭시 -, + 버튼이 비활성화 되는지 확인
const plusButtonElement = screen.getByTestId('plus-button');
const minusButtonElement = screen.getByTestId('minus-button');
expect(plusButtonElement).toBeDisabled();
expect(minusButtonElement).toBeDisabled();
});
});
screen 객체를 사용하여 테스트 할 요소를 가져오고 expect안에 해당 객체를 인자로 넘겨준 뒤 예상되는 테스트 결과를 matcher 함수로 테스트를 진행한다. Jest 에서 제공하는 matcher 함수는 공식 레퍼런스(https://runebook.dev/ko/docs/jest/expect)에 다양한 종류의 함수와 자세한 설명이 잘 정리되어 있으니 참고.