테스트 주도 개발로서 서비스의 안정적인 운영과 서비스의 품질을 확보하기 위해 테스트 코드를 작성하고 관리하기 위해서 사용됩니다.
TDD를 하기 위해서는 다음의 프로세스가 요구됩니다.
1. 자동화된 테스트 시스템 준비 (e.g. jest, react-testing-library)
2. 프로젝트의 기능에 대한 사양을 정리합니다.
3. 해당 사양에 대한 테스트 명세를 작성하고 실패하는 것을 확인합니다.
4. 실패한 테스트 명세를 통과시키기 위해 최대한 간단하고 빠르게 기능을 개발합니다.
5. 개발한 내용이 테스트 명세를 통과시키는 것을 확인하였다면 구현한 기능을 리팩토링합니다.
6. 위의 과정을 반복합니다.
이해를 돕기 위해 Header 컴포넌트를 TDD로 개발하는 과정을 예로 들어보려고 합니다.
react-create-app으로 만든 프로젝트로 jest와 react-testing-library가 설치되어 있다고 가정합니다.
테스트 코드에서 사용되는 메서드들에 대한 설명은 생략합니다.
적절한 위치에 Header.test.tsx 파일을 생성하고 아래의 코드를 입력합니다.
import { render, screen } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Header } from '.';
describe('<Header />', () => {
it('renders component correctly', () => {
const history = createMemoryHistory();
history.push('/');
const { container } = render(
<Router location={history.location} navigator={history}>
<Header />
</Router>,
);
const label = screen.getByText('할 일 목록');
expect(label).toBeInTheDocument();
});
});
이렇게 파일을 생성하고 저장 후 npm run test
로 테스트 코드를 실행하면 테스트가 실패하는 것을 볼 수 있습니다.
당연한 결과입니다. 우리는 아직 Header 컴포넌트 자체도 생성하지 않았기 때문입니다.
TDD에서는 실패가 발생하는 테스트 명세 작성 후 이를 해결하기 위한 가장 간단하고 빠른 코드를 작성합니다. 만약, 그 방법이 하드 코딩이라고 해도 괜찮습니다.
적절한 위치에 Header.tsx 파일을 생성합니다.
const Header = () => {
return (
<div>
<div>할 일 목록</div>
</div>
);
};
export default Header;
이렇게 파일을 수정 후 저장하면 새롭게 만든 Header 컴포넌트를 찾지 못해 여전히 테스트 명세가 실패라고 뜹니다.
그러면 테스트가 실행 중인 터미널을 열고 w키를 누른 후 a키를 눌러 모든 테스트 명세를 다시 실행시킵니다.
import React from "react";
import { render, screen } from "@testing-library/react";
import { Router } from "react-router-dom";
import { createMemoryHistory } from "history";
import Header from ".";
describe("<Header />", () => {
it("renders component correctly", () => {
const history = createMemoryHistory();
history.push("/");
const { container } = render(
<Router location={history.location} navigator={history}>
<Header />
</Router>
);
const label = screen.getByText("할 일 목록");
expect(label).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it("renders component correctly with /add URL", () => {
const history = createMemoryHistory();
history.push("/add");
const { container } = render(
<Router location={history.location} navigator={history}>
<Header />
</Router>
);
const label = screen.getByText("할 일 추가");
expect(label).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it("renders component correctly with /detail/:id URL", () => {
const history = createMemoryHistory();
history.push("/detail/1");
render(
<Router location={history.location} navigator={history}>
<Header />
</Router>
);
const label = screen.getByText("할 일 상세");
expect(label).toBeInTheDocument();
});
it("renders component correctly with NotFound", () => {
const history = createMemoryHistory();
history.push("/not_found");
render(
<Router location={history.location} navigator={history}>
<Header />
</Router>
);
const label = screen.getByText("에러");
expect(label).toBeInTheDocument();
});
});
Header 컴포넌트가 /, /add, /detail/:id, 그 이외의 url에서 제목을 제대로 표시하는지 확인하기 위한 테스트 코드를 추가합니다.
Header.test.tsx를 수정하고 다시 테스트하면 실패하게 됩니다.
이 예제에서는 스타일링을 전혀 고려하지 않고 있지만 스타일링을 적용할 경우 기존의 디자인에서 변경되었는지를 인지하지 위해서 스냅샷을 활용할 수 있습니다.
expect(...).toMatchSnapshot()
메서드를 통해 확인 가능합니다.
앞의 과정을 다시 반복하여 테스트를 통과할 수 있도록 기능 코드를 수정합니다.
import React from "react";
import { useLocation } from "react-router-dom";
const Header = () => {
const { pathname } = useLocation();
let title = "에러";
if (pathname === "/") {
title = "할 일 목록";
} else if (pathname === "/add") {
title = "할 일 추가";
} else if (pathname.startsWith("/detail")) {
title = "할 일 상세";
}
return (
<div>
<div>{title}</div>
</div>
);
};
export default Header;
중간 과정을 생략하긴 하였지만 이렇게 Header 컴포넌트를 TDD로 개발해보았습니다.
제가 TDD를 맛보기해보면서 느낀 점은 '테스트 코드도 코드다'라는 어찌 보면 당연한 생각입니다.
테스트 코드도 테스트를 위한 메서드들이 존재하고 DOM을 탐색하여 테스트 성공 / 실패 여부를 파악하다보니 실제 기능 코드와는 다르게 화면 어디선가 값을 보여주고 그것이 화면 안에 존재하는지 여부에 따라 참, 거짓 여부를 판단하게 됩니다.
당장은 테스트 코드에 익숙치 않아서 간단한 컴포넌트를 TDD로 개발하는 것에도 시간이 좀 소요되었지만, 익숙해질수록 개발자가 알아차리기 어려운 부분도 테스트를 자동화해줌으로써 개발 능률을 향상시키는 데에 도움이 되리라고 생각합니다.