이번 작업에서는 TDD 개발 흐름에 따라서, 브라우저 확인은 마지막에 해볼 예정
새 프로젝트를 생성하고
$ npx create-react-app rtl-tdd-todos
해당 디렉터리에서 필요한 라이브러리들을 설치 후
$ yarn add @testing-library/react @testing-library/jest-dom @types/jest
생성된 setupTest.js
에 import 해준다.
(9.07 수정)
import "@testing-library/react/cleanup-after-each";
import "@testing-library/jest-dom/extend-expect";
첫번째는 각 테스트가 DOM에 렌더링해놓은 내용들을
테스트가 끝날 때 다음 테스트를 위해서 지워주는 것이고,
두 번째는 jest-dom가 제공하는 matcher를 Jest 테스트 러너에게 인식시키는 것이다.
먼저 Todolist를 만들기 위해 어떤 컴포넌트와 기능들이 필요한지 계획을 세운다.
TDD개발 흐름으로 만든다는게, 유닛테스트들을 거쳐서
마지막에 통합테스트로 최종점검(?) 하는 흐름인지는 잘 모르겠지만 😓
작은(자식) 컴포넌트 -> 큰(부모) 컴포넌트 순으로 작업을 할 예정이다.
input
과 button
으로 이루어진 form
, 거기서 submit 이벤트가 발생하면 새로운 항목이 추가되어야 함props
로 받아서 렌더링 해줌전체적인 작업은 컴포넌트 틀 생성 -> 테스트코드 작성 -> 코드 통과시키기 -> 리팩토링 순으로 진행된다.
src 디렉터리에 TodoForm.js
빈 틀을 작성한 후 테스트코드로 UI를 구성해준다.
TodoForm.test.js
import React from "react";
import { render } from "@testing-library/react";
import TodoForm from "./TodoForm";
describe("<TodoForm />", () => {
// input과 button의 유무를 확인
it('has input and a button', () => {
const { getByText, getByPlaceholderText } = render(<TodoForm />);
getByPlaceholderText('할 일을 입력하세요');
getByText('등록');
});
});
이렇게 작성된 테스트코드를 확인하기 위해서는
$ yarn test
를 입력하면 terminal에 결과창이 뜨게 된다.
예시로, 이전에 캡쳐해둔 사진 사용하기 ㅎ
테스트 케이스가 통과하게 되면
실패하게 되면
현재로서는 위의 코드만 작성하면 당연히 테스트케이스가 실패로 나온다.
통과 시키기 위해서 TodoForm.js
를 수정해준다.
TodoForm.js
import React from 'react';
const TodoForm = () => {
return (
<form>
<input placeholder="할 일을 입력하세요" />
<button type="submit">등록</button>
</form>
);
};
export default TodoForm;
이렇게 코드 작성후 저장을 하면 테스트케이스가 통과로 뜨게 된다.
fireEvent
를 사용하여 input
에 change 이벤트를 발생시키면 value
값이 바뀌게 만든다.
TodoForm.test.js
...
it('changes input', () => {
const { getByPlaceholderText } = render(<TodoForm />);
const input = getByPlaceholderText('할 일을 입력하세요');
fireEvent.change(input, {
target: {
value: 'TDD 배우기'
}
});
// toHaveAttribute는 해당 DOM에 특정 속성이 있는지 확인해줌
expect(input).toHaveAttribute('value', 'TDD 배우기');
});
실패한 테스트코드를 성공시키기 위해서 input
상태에 대한 코드를 추가해준다.
TodoForm.js
import React, { useState, useCallback } from 'react';
const TodoForm = () => {
const [value, setValue] = useState('');
const onChange = useCallback(e => {
setValue(e.target.value);
}, []);
return (
<form>
<input
placeholder="할 일을 입력하세요"
value={value}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
);
};
export default TodoForm;
input
에 대한 상태관리가 마무리 되었으니 이번에는 button
클릭시에 발생하는
submit 이벤트를 관리할 차례.
TodoForm
컴포넌트는 onInsert
라는 함수를 props
로 받아오면서
submit 이벤트가 발생하면 호출될 것이다. 이 때 input
창은 비워져야 한다.
TodoForm.test.js
...
it('calls onInsert and clears input', () => {
const onInsert = jest.fn(); // mock함수 - 호출된 것을 쉽게 확인할 수 있는 함수
const { getByText, getByPlaceholderText } = render(
<TodoForm onInsert={onInsert} />
);
const input = getByPlaceholderText('할 일을 입력하세요');
const button = getByText('등록');
// 수정하고
fireEvent.change(input, {
target: {
value: 'TDD 배우기'
}
});
// 버튼 클릭
fireEvent.click(button);
expect(onInsert).toBeCalledWith('TDD 배우기'); // onInsert 가 'TDD 배우기' 파라미터가 호출됐어야함
expect(input).toHaveAttribute('value', ''); // input이 비워져야함
});
위의 테스트 코드를 통과시키기 위해 button 클릭을 위한 코드작업을 해준다.
TodoForm.js
import React, { useState, useCallback } from 'react';
const TodoForm = ({ onInsert }) => {
const [value, setValue] = useState('');
const onChange = useCallback(e => {
setValue(e.target.value);
}, []);
const onSubmit = useCallback(
e => {
onInsert(value);
setValue(''); // input창 비우기
e.preventDefault(); // 새로고침을 방지함
},
[onInsert, value]
);
return (
<form onSubmit={onSubmit}>
<input
placeholder="할 일을 입력하세요"
value={value}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
);
};
export default TodoForm;
이렇게 작업해주면 테스트코드에 pass가 뜨게 된다.
작성된 코드들을 확인해보면, 테스트코드쪽에서 반복되는 코드가 있기에 리팩토링을 해준다.
getByPlaceholderText('할 일을 입력하세요');
getByText('등록');
↓ ↓ ↓ ↓ ↓
TodoForm.test.js
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import TodoForm from './TodoForm';
describe('<TodoForm />', () => {
const setup = (props = {}) => {
const utils = render(<TodoForm {...props} />);
const { getByText, getByPlaceholderText } = utils;
const input = getByPlaceholderText('할 일을 입력하세요');
const button = getByText('등록');
return {
...utils,
input,
button
};
};
it('has input and a button', () => {
const { input, button } = setup();
expect(input).toBeTruthy(); // 해당 값이 truthy 한 값인지 확인
expect(button).toBeTruthy();
});
it('changes input', () => {
const { input } = setup();
fireEvent.change(input, {
target: {
value: 'TDD 배우기'
}
});
expect(input).toHaveAttribute('value', 'TDD 배우기');
});
it('calls onInsert and clears input', () => {
const onInsert = jest.fn();
const { input, button } = setup({ onInsert }); // props 가 필요 할땐 이렇게 직접 파라미터로 전달
// 수정하고
fireEvent.change(input, {
target: {
value: 'TDD 배우기'
}
});
// 버튼 클릭
fireEvent.click(button);
expect(onInsert).toBeCalledWith('TDD 배우기');
expect(input).toHaveAttribute('value', '');
});
setup
이란 함수를 새로 만들어서 그 안에서 input
과 button
을 미리 찾는 작업을 하고,
render 결과물 안에 들어있는 함수, 객체, input
, button
을 함께 반환해준다.
그리고 setup
함수에선 props
를 파라미터로 받아오며, 필요시에 props
를 넣을 수 있게 해준다.
이 작업까지 마치면 깔끔해진 코드들을 볼 수 있다 :)
다음 포스팅에서 TodoItem
작업을 이어가보자 -