tdd는 test driven development의 약자이며 소프트웨어 개발 방법론 중의 하나다.
보통 프로그램을 개발할 때 개발 후 테스트를 하는데 tdd는 테스트 후 개발을 하는 방식이다.
위의 3개 과정을 반복한다.
중요한건 성공하는 테스트 코드, 실패하는 테스트 코드를 모두 작성해야 한다.
그래야 불필요한 설계를 피할수있고 정확한 요구사항만 집중할 수 있기 때문이다!
위의 세 과정을 반복하면 모듈화가 자연스럽게 이루어진다.
그래서 의존성과 종속성이 낮은 모듈로 조합된 컴포넌트 개발이 가능하게 된다.
테스트 코드를 기반으로 컴포넌트를 작성하기 때문에
작성한 코드를 보지 않고도 테스트 코드에서 설계의 문제를 바로 찾아낼 수 있다.
TDD는 기본적으로 Unit Test 기반으로 테스트코드를 작성해서
만약 나중에 문제가 생기면 각각 모듈별로 테스트를 진행하면
문제가 어디서 발생했는지 쉽게 찾을 수 있다.
보통 개발 후 테스트를 진행하면 보통 Integration Test에 지나지 않는다.
근데 TDD로 테스트를 자동화 시키면 정확한 테스트 근거를 산출할 수 있게된다!
TDD를 프로젝트에 도입하려면 필요한 지식을 습득하고 개발환경을 구축하는데 시간이 많이 든다.
저번에 TDD를 배우려고 했다가 1년정도 방치한 이유도 학습하기가 번거로웠기 때문이다...
프로젝트를 진행할 때 코드의 어느 부분에서 예외상황이 발생할 지 모이기도 한다.
만약 개발기간이 짧다면 테스트 코드 작성 후 통과하기 위한 코드를 작성하면 비효율적일수도 있을것이다.
문서화 작업을 시도하고 있는데 상당히 시간이 많이 드는 일이다. TDD도 시간이 많이들고 문서화 작업도 시간이 많이 들면 테스트 코드를 작성하는게 훨 낫다 생각했기에 다시 배우게 됐다.
그리고 코드를 리팩토링 하거나 설계가 바뀌어야 할때 초반에 잘못된 설계로 인해 에러가 발생할 때가 많았다. 그래서 이 부분에 들어가는 시간을 줄이기 위해 배우게됐다.
무언가 새로운 기술, 프레임워크 등을 배울 때 TodoList로 작성하는걸 좋아한다.
새로운 언어를 배울 때 Hello World 를 찍어보는것과 비슷한 이유라 생각된다.
그래서 이번에 TDD를 배울때도 TodoList를 작성해보려 한다.
이번 글에서는 todo 를 추가하는것 까지만 하고
그 전에 아래 명령어로 프로젝트 생성 후 작성을 시작했다.
▶ npx create-react-app tdd --template=typescript
만약 cra를 사용하지 않을경우 아래의 명령어로 설치하면 된다.
▶ npm install --save-dev @testing-library/react
혹은
▶ yarn add --dev @testing-library/react
TodoList는 크게 아래와 같은 컴포넌트로 나눌 수 있다.
<TodoItem>
컴포넌트: 1개의 할일을 보여주는 컴포넌트<TodoList>
컴포넌트: <TodoItem>
컴포넌트로 이루어진 할일 목록을 보여주는 컴포넌트<TodoForm>
컴포넌트: <input>
, <button>
으로 TodoList에 할일을 추가하는 컴포넌트<TodoApp>
컴포넌트: <TodoList>
, <TodoForm>
을 그려주고 todo item은 props로 내려주는 item을 그려주기만 하면 될것같다.
테스트 할때 도 item이 span으로 제대로 그려지는지 test하면 된다.
// TodoItem.test.tsx
import { render, screen } from "@testing-library/react";
import TodoItem from "./TodoItem";
describe('<TodoItem/>', () => {
it('has span & button', () => {
const mockItem = "this is a item";
render(<TodoItem item={mockItem}/>)
expect(screen.getByText(mockItem)).toBeInTheDocument();
})
})
render로 해당 컴포넌트를 그려준다.
그 후 화면에 mockItem 내용이 제대로 그려졌는지 확인하는 테스트 코드이다.
테스트 코드를 작성하고 터미널에서 npm test
를 하면 테스트 코드의 통과여부를 볼 수 있다.
아직 TodoItem을 통과하게 작성해주지 않았기에 FAIL
이라 나올것이다.
// TodoItem.tsx
const TodoItem = ({item}: {item: string}) => {
return (
<>
<span>{item}</span>
</>);
}
export default TodoItem;
이제 테스트 코드가 통과하게 코드를 작성해보자.
props 로 내려준 item을 span으로 그려주었다.
코드를 작성후 저장을 눌러주면 TodoItem.test.tsx 는 초록색으로 PASS
라 뜨는걸 볼 수있다.
todolist 컴포넌트는 todoList를 입력받으면 해당 내용들을 화면에 뿌려주면 된다.
//TodoList.test.tsx
import { render, screen } from "@testing-library/react"
import TodoList from "./TodoList"
describe('<TodoList/>', () => {
const todoItems = [
{id: 1, item: 'item1'},
{id: 2, item: 'item2'}
];
it('render with todos', () => {
render(<TodoList todoItems={todoItems}/>);
expect(screen.getByText(todoItems[0].item)).toBeInTheDocument();
expect(screen.getByText(todoItems[1].item)).toBeInTheDocument();
});
})
TodoItem과 마찬가지로 props를 넣어서 render()
해준다.
그 후 실제로 화면에 그려졌는지 expect.toBeInTheDocument()
를 사용해 확인해준다.
이때도 역시 FAIL
이 뜬다.
import TodoItem from "./TodoItem";
const TodoList = ({todoItems}:{todoItems: {id: number, item: string}[]}) => {
return (
<ul data-testid="TodoList">
{
todoItems.map(({id, item}) =>(
<TodoItem key={id} item={item}/>
))
}
</ul>);
}
export default TodoList;
위와같이 props로 내려온 todoItems를 map으로 그려주면 테스트코드는 통과하게 된다.
위에서 작성한것과 같이 다음 순서는 TodoForm
이다.
TodoForm 안에는 input
과 button
이 존재하게 된다.
그리고 props로 input
값이 변경되었을 때 바꿔주는 onChange
함수,
button
이 클릭되었을 때 onSubmit
함수를 호출하고, input의 값은 비어지게 된다.
테스트 해봐야 할건 세개정도 인것같다.
onSubmit(input값)
이 호출되고, input값이 비어지는지.아래 코드에서 이걸 테스트 해보려 한다.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"
describe('<TodoForm/>', () => {
it('<input>, <button> 렌더링 테스트', () => {
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
expect(screen.getByTestId('TodoFormInput')).toBeInTheDocument();
expect(screen.getByTestId('TodoFormButton')).toBeInTheDocument();
});
it('input 변경', async () => {
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
const inputVal = 'test input';
await userEvent.type(input, inputVal);
expect(input.value).toBe(inputVal);
});
it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
const onSubmit = jest.fn();
const inputVal = 'test input Val';
render(<TodoForm onSubmit={onSubmit}/>);
const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
await userEvent.type(input, inputVal);
await userEvent.click(button);
expect(onSubmit).toBeCalledWith(inputVal);
expect(input.value).toBe("");
})
});
여기서는 유저가 inputVal
을 입력했을 때 input의 값의 inputVal
이 되는지 확인한다.
여기서는 유저가 입력후 버튼을 눌렀을 때 함수에 inputVal
이 같이 넘어가는지,
그리고 input의 값이 비어지는지 확인한다.
import { useState} from 'react'
interface TtodoForm {
onSubmit: (value: string) => void;
}
const TodoForm = ({onSubmit}: TtodoForm) => {
const [value,setValue] = useState('')
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
onSubmit(value);
setValue('');
e.preventDefault();
}
return <>
<form onSubmit={handleOnSubmit}>
<input
data-testid="TodoFormInput"
placeholder="할 일을 입력"
onChange={handleOnChange}
value={value}
/>
<button data-testid="TodoFormButton">등록</button>
</form>
</>;
}
export default TodoForm;
이제 드디어 마지막까지 왔다.
TodoApp만 작성하면 1차적으로 끝나게 된다.
TodoApp에서 테스트 할건 간단한다.
input
에 값이 입력되고를 확인하면 된다.
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event";
import TodoApp from "./TodoApp"
describe('<TodoApp/>', () => {
it('todoform, todolist render', () => {
render(<TodoApp/>)
screen.getByTestId('TodoFormButton')
screen.getByTestId('TodoList');
});
it('new todo', async() => {
render(<TodoApp/>)
const input = screen.getByTestId("TodoFormInput");
const button = screen.getByTestId("TodoFormButton");
const todoList = screen.getByTestId("TodoList");
await userEvent.type(input, "new todo");
await userEvent.click(button);
expect(todoList).toHaveTextContent("new todo");
});
});
import { useRef, useState } from 'react';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
const TodoApp = () => {
const [state, setState] = useState<{id: number, item: string}[]>([]);
const nextId = useRef(1);
const handleOnSubmit = (todo: string) => {
setState(prev => ([...prev, {id: nextId.current++, item: todo}]));
}
return (
<div data-testid="TodoApp">
<TodoForm onSubmit={handleOnSubmit}/>
<TodoList data-testid="TodoList" todoItems={state}/>
</div>
);
};
export default TodoApp;
여기 TodoForm 테스트 코드를 작성하는 부분을 보면 아래와 같같이 중복이 매우 많이 보인다.
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
해당 코드는 setup
이라는 함수를 만들어서 중복을 없애줄 수 있다.
그렇게 중복되는 부분을 setup 함수로 작성해주면
mport { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"
describe('<TodoForm/>', () => {
it('<input>, <button> 렌더링 테스트', () => {
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
expect(screen.getByTestId('TodoFormInput')).toBeInTheDocument();
expect(screen.getByTestId('TodoFormButton')).toBeInTheDocument();
});
it('input 변경', async () => {
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
const inputVal = 'test input';
await userEvent.type(input, inputVal);
expect(input.value).toBe(inputVal);
});
it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
const onSubmit = jest.fn();
const inputVal = 'test input Val';
render(<TodoForm onSubmit={onSubmit}/>);
const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
await userEvent.type(input, inputVal);
await userEvent.click(button);
expect(onSubmit).toBeCalledWith(inputVal);
expect(input.value).toBe("");
})
});
위와같이 중복이 매우 많았던 코드에서
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TodoForm from "./TodoForm"
describe('<TodoForm/>', () => {
const setup = () => {
const onSubmit = jest.fn();
render(<TodoForm onSubmit={onSubmit}/>);
const input = screen.getByTestId('TodoFormInput') as HTMLInputElement;
const button = screen.getByTestId('TodoFormButton') as HTMLButtonElement;
return {
input, button, onSubmit
};
}
it('<input>, <button> 렌더링 테스트', () => {
const {input, button} = setup();
expect(input).toBeInTheDocument();
expect(button).toBeInTheDocument();
});
it('input 변경', async () => {
const {input} = setup();
const inputVal = 'test input';
await userEvent.type(input, inputVal);
expect(input.value).toBe(inputVal);
});
it('onInsert 함수가 불러지면 input값은 비워짐', async () => {
const {input, button, onSubmit} = setup();
const inputVal = 'test input Val';
await userEvent.type(input, inputVal);
await userEvent.click(button);
expect(onSubmit).toBeCalledWith(inputVal)
expect(input.value).toBe("");
})
});
위와같이 중복도 없어지고, 보기 조금 더 편해진 코드로 바꿀수가 있다.