해당 포스팅에는 과제에 대한 코드가 포함되어 있습니다!
3주차에는 TDD(테스트 주도 개발)을 적용하여, 2주차에 만들었던 Todo 앱을 테스트 하는 것이었다.
TDD를 할 때, RED → GREEN → Refactor 이와 같은 사이클을 짧은 주기로 반복하는 것이다. Red에서는 통과하지 못하는 테스트를 작성한 다음에 Green 단계에서 테스트를 통과하도록 작성하고, Refactoring을 한다.
Jest는 단순함에 초점을 맞춘 자바스크립트 테스팅 프레임워크이다.
설치방법
$ npm i -D jest @types/jest babel-jest
리액트 테스팅 라이브러리는 사용자와 동일한 방식으로 DOM 쿼리를 사용할 수 있게 해준다.
실제 사용자가 우리의 앱을 사용하는 방식으로 테스트하여 우리의 앱이 올바르게 동작하는지 테스트 할 수 있다.
설치방법
$ npm i -D @testing-library/react @testing-library/jest-dom
jest.setup.js
파일을 만들어서 jest.config.js
에 setupFilesAfterEnv
에 설정을 해주면 편리하게 사용할 수 있다.// jest.setup.js 파일
import '@testing-library/jest-dom'
// jest.config.js 파일
module.exports = {
setupFilesAfterEnv: [
'jest-plugin-context/setup', // 이건 context 구문을 사용하기 위한 설정
'./jest.setup', // jest setup 파일
],
};
$ npx jest
테스트를 실행할 수 있고, 계속해서 모든 테스팅 파일을 감시하길 바란다면 watchAll
을 사용하면 된다.
$ npx jest --watchAll
$ npx jest --watchAll --verbose
verbose
해당 명령어를 사용하면 계층 구조화되어 볼 수 있다.
모든 컴포넌트에 테스트를 작성하고, coverage 100%를 달성하는 게 과제의 목표였다.
import { render, fireEvent } from '@testing-library/react';
import App from './App';
describe('App', () => {
function renderApp() {
return render((<App />));
}
it('App이 렌더링된다.', () => {
const { container } = renderApp();
expect(container).toHaveTextContent('To-do');
expect(container).toHaveTextContent('할 일이 없어요!');
expect(container).toHaveTextContent('추가');
});
it('할 일을 입력시 handleChange 함수가 실행되어 input의 value가 변경된다.', () => {
const { getByLabelText } = renderApp();
fireEvent.change(getByLabelText('할 일'), {
target: { value: 'TDD 하기' },
});
expect(getByLabelText('할 일').value).toBe('TDD 하기');
fireEvent.change(getByLabelText('할 일'), {
target: { value: '집에 가기' },
});
expect(getByLabelText('할 일').value).toBe('집에 가기');
});
it('추가 버튼을 누르면 handleClickAdd 함수가 실행되어 할 일이 렌더링된다.', () => {
const { container, getByLabelText, getByText } = renderApp();
fireEvent.change(getByLabelText('할 일'), {
target: { value: 'TDD 하기' },
});
expect(getByLabelText('할 일').value).toBe('TDD 하기');
fireEvent.click(getByText('추가'));
expect(container).toHaveTextContent('TDD 하기');
});
it('완료 버튼을 누르면 handleClickDelete 함수가 실행되어 추가되었던 할 일이 삭제된다.', () => {
const { container, getByLabelText, getByText } = renderApp();
fireEvent.change(getByLabelText('할 일'), {
target: { value: 'TDD 하기' },
});
fireEvent.click(getByText('추가'));
fireEvent.click(getByText('완료'));
expect(container).not.toHaveTextContent('TDD 하기');
});
});
import { render, fireEvent } from '@testing-library/react';
import Input from './Input';
describe('Input', () => {
const handleChange = jest.fn();
const handleClick = jest.fn();
const renderInput = () => render(
<Input
value="운동하기"
onChange={handleChange}
onClick={handleClick}
/>,
);
it('Input과 button이 렌더링된다.', () => {
const { container } = renderInput();
expect(container).toHaveTextContent('할 일');
expect(container).toHaveTextContent('추가');
});
it('input에 입력시 handleChange 함수가 실행된다.', () => {
const { getByLabelText } = renderInput();
const input = getByLabelText('할 일');
fireEvent.change(input, { target: { value: 'TDD 하기' } });
expect(handleChange).toBeCalled();
});
it('추가 버튼을 클릭하면 handleClick 함수가 실행된다.', () => {
const { getByText } = renderInput();
expect(handleClick).not.toBeCalled();
fireEvent.click(getByText('추가'));
expect(handleClick).toBeCalled();
});
});
import { render, fireEvent } from '@testing-library/react';
import Item from './Item';
test('Item', () => {
const task = {
id: 1,
title: '뭐라도 하기',
};
const handleClick = jest.fn();
const { container, getByText } = render((
<Item
task={task}
onClickDelete={handleClick}
/>
));
expect(container).toHaveTextContent('뭐라도 하기');
expect(container).toHaveTextContent('완료');
expect(handleClick).not.toBeCalled();
fireEvent.click(getByText('완료'));
expect(handleClick).toBeCalledWith(1);
});
import { fireEvent, render } from '@testing-library/react';
import List from './List';
import tasks from '../fixtures/tasks';
describe('List', () => {
const handleClick = jest.fn();
const renderList = (todoList) => render(
<List
tasks={todoList}
onClickDelete={handleClick}
/>,
);
context('tasks가 있을 경우', () => {
it('List 컴포넌트가 렌더링된다.', () => {
const { container } = renderList(tasks);
expect(container).toHaveTextContent('리액트 공부하기');
expect(container).toHaveTextContent('블로그 작성하기');
});
it('완료 버튼을 누르면 handleClick 함수가 실행된다.', () => {
const { getAllByText } = renderList(tasks);
const buttons = getAllByText('완료');
fireEvent.click(buttons[0]);
expect(handleClick).toBeCalled();
});
});
context('tasks가 없을 경우', () => {
const emptyTasks = [];
it('할 일이 없어요 메시지가 보인다.', () => {
const { container } = renderList(emptyTasks);
expect(container).toHaveTextContent('할 일이 없어요!');
});
});
});
import { render, fireEvent } from '@testing-library/react';
import Page from './Page';
import tasks from '../fixtures/tasks';
describe('Page', () => {
const handleChangeTitle = jest.fn();
const handleClickAddTask = jest.fn();
const handleClickDeleteTask = jest.fn();
function renderPage(taskTitle = '') {
return render(
(<Page
tasks={tasks}
taskTitle={taskTitle}
onChangeTitle={handleChangeTitle}
onClickAddTask={handleClickAddTask}
onClickDeleteTask={handleClickDeleteTask}
/>),
);
}
it('Page가 렌더링된다.', () => {
const { container, getAllByText } = renderPage();
expect(container).toHaveTextContent('리액트 공부하기');
expect(container).toHaveTextContent('블로그 작성하기');
const buttons = getAllByText('완료');
fireEvent.click(buttons[0]);
});
});
여러 곳에서 사용되는 테스트용 데이터를 fixture라고 한다. 테스트용 데이터를 관리하는 fixtures 폴더를 생성하여, tasks라는 파일을 만들어서 관리했다.
// tasks.js
const tasks = [
{
id: 1,
title: '리액트 공부하기',
},
{
id: 2,
title: '블로그 작성하기',
},
];
export default tasks;
참고 - • https://johngrib.github.io/wiki/junit5-nested/#describe---context---it-%ED%8C%A8%ED%84%B4
Describe
-Context
- It
은 상황을 설명하기보다는 테스트 대상을 주인공 삼아 행동을 더 섬세하게 설명하는데 적합하다.
키워드 | 설명 | |
---|---|---|
Describe | 설명할 테스트 대상을 명시한다 | |
Given에 해당. 테스트할 대상을 지정할 때 사용하는 키워드 | 테스트 대상이 되는 클래스, 메소드 이름을 명시한다 | |
Context | 테스트 대상이 놓인 상황을 명시한다. | |
조건을 정의할 때 사용한다. When에 해당하는 부분 | 테스트할 메소드에 입력할 파라미터를 설명한다 | |
It | 테스트 대상의 행동을 설명한다 | 테스트 대상 메소드가 무엇을 리턴하는지 설명한다 |
화살표 함수에서 () => {}
중괄호를 해주는 편이 가독성이 더 좋다.
props가 2개 이상이면 세로 정렬해주기. (왜 항상 세로 정렬하는 건 까먹을까?)
여러 곳에서 사용되는 테스트용 데이터를 보통 fixture라고 한다. fixtures
폴더 하나를 만들어서 따로 관리하는 게 좋다.
const handleChange = jest.fn()
등 이러한 것을 describe('컴포넌트 이름') 밑에 선언해두는 편이 낫다.
3주차 TDD를 들어가면서 너무 힘들었다.
처음 언어를 배우는데, 참 직관적인 단어들을 사용하지만 이렇게 어려울수가...?!
무엇을 어떻게 테스트 해야 되는 지를 몰라서, 많이 헤맸었고, 테스트는 값을 가상으로 준다는 게 익숙치 않아서 힘들었다.
아무튼 하는 동안 react-testing-library 공식문서랑 jest의 공식문서를 정말 자주 방문했다. (앞으로도 많이 방문할 예정...🥺)
지금 4주차 과제를 하는 중인데, 왜 다시 리셋된 느낌일까? 리덕스를 사용했다고 이렇게 달라 보일 수가 있는 걸까. 슬프다.