TDD(Test-driven Development)
TDD는 코드를 작성하기 전에 테스트를 쓰는 소프트웨어 개발 방법론이다. 개발자 자신이 바람직하다고 생각하는 코드의 결과를 미리 정의하고 이를 바탕으로 코드를 작성하는 방법으로, TDD를 통해 소프트웨어를 개발하는 것은 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 작성하는 과정을 반복하는 것을 의미한다.
TDD의 개발 주기를 그림으로 나타내면 위와 같이 총 3단계로 이루어진다.
- Write Failing Test : 실패하는 테스트 코드를 먼저 작성한다.
- Make Test Pass : 테스트 코드를 성공시키기 위한 실제 코드를 작성한다.
- Refactor: 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.
이 과정에서 1의 과정을 마치기 전에 2의 작업을 시작하지 않도록 주의해야 한다. 또한 2를 진행할 때는, 1의 테스트를 통과할 정도의 최소 코드만 작성해야 한다. 테스트를 먼저 작성하는 것은 필연적으로 코드를 어떻게 구성할지 고민하게 된다는 것을 의미하고, 결과적으로 버그가 더 적은 코드를 짤 수 있게 된다. 또 불필요한 설계를 피할 수 있고, 테스트 코드의 요구 사항에 집중할 수 있게 된다. 일반적으로 TDD를 따라 소프트웨어를 개발할 경우 그렇지 않은 경우보다 결함을 50~90%까지 감소시킬 수 있다.
버그가 적은, 보다 효과적인 코드를 짤 수 있는 방법이지만 실제로 완전한 TDD를 따르는 개발자는 이외로 많지 않다. 그 이유는 대부분의 개발자들이 생각하고 일하는 방식과 일치하지 않기 때문이다. 대부분의 개발자는 테스트를 작성하는 것 보다, 만들어야 하는 것을 바로 코드로 작성하는 방식이 훨씬 자연스럽고 빠르다고 느낄 것이다. 많은 개발자들에게 왜 TDD를 따르지 않는지 물어보면, 대부분 ‘속도’ 때문이라고 대답할 것이다.
TDD를 사용하는 이유
코드를 작성하기 전에 테스트 코드를 먼저 작성하는 것이 시간이 오래 걸리는 것 처럼 느껴지지만, 오히려 예상하지 못했던 버그를 줄여 소요 시간을 줄일 수 있기 때문이다. 개발 과정에서 코드는 다양한 조건에 의해 계속 삽입/수정/삭제 되는데, 이 과정에서 코드가 중복되거나 불필요한 코드가 남게 된다. 그리고 이로 인해 여러 버그가 발생하거나, 디버깅 또한 어려워지기도 한다. 결국 그런 코드를 유지보스하기 위해서 처음 개발할 때 아꼈던 리소스보다 더 많은 리소스를 투입해야 하는 경우가 발생한다.
당장 완전한 TDD를 따르는 것은 아주 어려운 일이지만, 작성하려는 코드에 대해 특정한 규칙(테스트)를 설정하기 위해 고민하면서 코드가 큰 틀에서 어떤 의미를 갖는지 고민하는 것은 개발자로서 성장하는데 중요한 도구가 될 수 있다.
테스트 코드를 작성하는 방법
console.log
를 통해 현재 작성중인 코드가 어떤 결과물을 도출하는지 확인하는 것도 일종의 테스트이다. 여러 개발자들이 더 나은 테스트를 작성하기 위해 많은 테스트 오픈소스 프레임워크를 제작했으며, 대표적으로는 mocha라는 테스트 프레임워크와 chai라는 라이브러리를 사용한다.
React 환경에서 테스트하기
React에서 테스트는 Testing Library, Jest를 이용할 수 있다. Testing Libarary에서 React용 React Testing Library을 제공하고 있기 때문에, create-react-app을 이용하여 React 프로젝트를 생성하면 자동으로 Testing Libarary를 이용할 수 있으며, 이렇게 설치한 Testing Library는 실행하고 싶은 컴포넌트나 클릭 이벤트 등 다양한 곳에 쓸 수 있다.
Jest는 JavaScript의 Testing Framework / Test Runner로 테스트 파일을 자동으로 찾아 테스트를 실행하고, 테스트를 실행한 결과 기대만큼 올바른 값을 가지고 있는지 함수를 이용하여 체크해 테스트가 성공인지 실패인지 판단해준다.
Testing Libarary와 Jest는 역할이 각각 다르기 때문에 React에 대한 테스트를 진행할 때는 어느 한쪽 라이브러리를 이용하는 것만으로는 테스트를 할 수 없다.
React 기본 테스트 환경 확인
- 새로운 React 프로젝트 생성하기
CRA(
npx create-react-app
)를 통해 React 프로젝트를 생성하면 테스트 환경이 설정되어 바로 테스트가 가능하다. 프로젝트 생성 후 폴더로 이동하여package.json
파일을 확인하면dependencies
안에@testing
이라는 접두어가 붙은 3개의 라이브러리를 확인할 수 있는데, 이것들이 바로 테스트를 수행할 때 이용하는 라이브러리이다.scripts
안에test
가 있기 때문에, 테스트를 실행할 때는npm run test
를 이용하면 된다.CRA로 React 프로젝트 설치 시 기본으로 설치되는 테스트 모듈
- @testing-library/jest-dom : Jest-dom이 제공하는 custom matcher를 사용할 수 있게 해준다.
- @testing-library/react : 컴포넌트의 요소를 찾기 위한 query가 포함되어 있다.
- @testing-library/user-event : click 등 사용자 이벤트에 이용된다.
- 테스트 파일 확인하기
src
폴더 안을 보면,setupTests.js
와App.test.js
라는 이름의 파일을 확인할 수 있다.App.test.js
에는 간단한 테스트가 이미 만들어져 있고, 작성한 프로젝트의package.json
파일과 파일의 구성을 보면 CRA 명령으로 프로젝트를 생성함으로써 테스트를 하기 위한 환경이 사전에 설치되어 있는 것을 알 수 있다.npm run test
명령어로 test를 실행하면 아래와 같은 화면이 나온다.실행을 하면 대화식 메시지가 나타나는데, 메시지를 보면 키보드 키에 따라 다양한 처리를 할 수 있다는 것을 알 수 있다.
Press a to run all tests.
라고 설명되어 있는 부분을 실행하기 위해 a키를 눌러 보면 아래의 화면처럼 테스트가 실행되고 실행한 테스트에 성공했음을 의미하는 PASS 메세지와 테스트 건수나 테스트에 걸린 시간 등도 확인할 수 있다.App.test.js
에는 Jest 함수로 테스트를 실행할 때 반드시 이용하는 함수인test
함수가 작성되어 있다.test
함수의 첫 번째 인자는 테스트가 어떤 내용인지 나중에 다시 읽어도 테스트 내용을 알 수 있는 설명을 작성하고, 두 번째 인자는 하고자 하는 테스트를 함수의 형태로 넣는다.전달인자로 들어간 함수를 분석해보면 아래와 같다.
- 첫번째 줄은 테스트하고자 하는 컴포넌트를
render()
함수로 전달하고 있다. eact-testing-library에서는 테스트를 진행할 컴포넌트를render()
함수의 인자로 전달한다.
- 두번째 줄은
screen
의 다양한 메서드 중getByText()
메서드를 이용해 "learn react"라는 문자열이 있는지 확인하여linkElement
에 할당하고 있다. (여기서i
는 정규식 표현으로i
를 붙임으로써 대소문자를 구분하지 않게 만들어준다)
- 세번째 줄은
expect
함수의 인자로 지정한 요소가document.body
에 존재하는지toBeInTheDocument
함수를 사용하여 체크하고 있다. 여기서toBeInTheDocument
함수는 matchers 함수라고 부른다.
App.test.js 파일 중 이용되고 있는
test
함수,expect
함수는 Jest의 함수고,toBeInTheDocument
는 jest-dom 라이브러리에 포함된 Custom matchers이다. jest-dom은src
폴더의setupTests.js
파일 내에서 import 되고 있다. 이 파일에서 import를 삭제하면toBeInTheDocument
함수를 이용할 수 없다.테스트로 확인한 부분 App.js 파일을 보면
<a>
태그의 콘텐츠로 "Learn React"를 찾을 수 있는데, App.test.js의 내부에서 screen의getByText
메서드로 찾아낸 부분이 바로 이 부분이다. 만약App.js
에서 Learn React가 일부 변경이 된다면 테스트는 FAIL로 뜨고 실패하게 될것이다. - 첫번째 줄은 테스트하고자 하는 컴포넌트를
- 간단한 테스트 직접 만들기
React를 이용해 테스트를 실행하는 것은 처음에 어렵게 느껴질 수 있기 때문에, Jest만을 이용해 테스트를 실행하고 테스트의 기본적인 규칙과 기술 방법을 확인해보자. 아까 보았던 것 처럼 테스트는
test
함수 안에 쓰고 첫번째 인자에는 테스트에 대한 설명, 두 번째 인자에는 테스트 내용을 함수의 형태로 작성한다.expect
함수의 인자로 2+2를 넣고toBe
함수에 4라는 결과를 지정하여 테스트를 해보면 통과하는 것을 확인할 수 있다.toBe
함수는 matchers 함수 중 하나로expect
함수에 지정한 값이toBe
함수에 지정한 값과 일치하는지 체크한다. 만약toBe
의 전달인자로 5를 넣으면 테스트는 통과하지 못한다.테스트 함수 대신
it
을 사용해도 같은 결과가 나오고,describe
함수를 사용하면it
이나test
함수를 하나의 파일에 여러개 포함할 수 있다.describe
는 Test Suites라고 불리며test
/it
함수 블록은 Test(Test case)라고 한다.
직접 만든 컴포넌트 테스트하기
- 컴포넌트 만들기
전원의 상태를 OFF에서 ON으로 전환하는 심플한 컴포넌트,
Light.jsx
를 생성하고 App.js에 import한다. 이 컴포넌트는 name을 prop으로 전달받는다.import { useState } from "react"; function Light({ name }) { const [light, setLight] = useState(false); return ( <div> <h1> {name} {light ? "ON" : "OFF"}{" "} </h1> <button onClick={() => setLight(true)} disabled={light ? true : false}> ON </button> <button onClick={() => setLight(false)} disabled={!light ? true : false}> OFF </button> </div> ); } export default Light;
import Light from "./components/Light"; import "./styles.css"; export default function App() { return ( <div className="App"> <Light name="전원" /> </div> ); }
- 컴포넌트 테스트 하기
Light.test.js
파일을 만들고 아래와 같이 테스트를 작성한다.name
을 props로 전달받아전원
이 올바르게 표시되어있는지 확인하는 테스트 코드이다. 올바르게 작성되었다면 PASS가 나오게 될 것이다.import { render, screen } from '@testing-library/react'; import Light from './Light'; it('renders Light Component', () => { render(<Light name="전원" />); const nameElement = screen.getByText(/전원 off/i); expect(nameElement).toBeInTheDocument(); })
다음은 OFF 버튼이 disabled로 되어있는지 matchers 함수의
toBeDisabled
함수를 이용해 테스트를 작성한다.getByRole
을 이용하여 button을 지정했고, 버튼이 2개이므로 옵션의name
을 이용하여 OFF 버튼을 찾는다. 현재는 OFF 버튼이 disabled 상태이기 때문에 테스트 역시 통과하게 된다. 만약 disabled가 아니라는 것을 테스트하려면toBeDisabled
함수 앞에not
을 붙이면 구현할 수 있다.it('off button disabled', () => { render(<Light name="전원" />); const offButtonElement = screen.getByRole('button', { name: 'OFF' }); expect(offButtonElement).toBeDisabled(); }) // disabled 상태가 아닌 것을 확인하는 테스트 it('on button enable', () => { render(<Light name="전원" />); const onButtonElement = screen.getByRole('button', { name: 'ON' }); expect(onButtonElement).not.toBeDisabled(); });
버튼 클릭의 이벤트 유무도 테스트로 구현 가능하다.
fireEvent
를 import하고,fireEvent
의click
메서드에 전달인자로 테스트하고자 하는 요소를 전달하면 된다.// 이벤트 후의 ON의 disabled 상태를 확인하는 테스트 it('change from off to on', () => { render(<Light name="전원" />); const onButtonElement = screen.getByRole('button', { name: 'ON' }); // fireEvent는 테스트 환경에서 클릭 이벤트를 구현한다. fireEvent.click(onButtonElement); expect(onButtonElement).toBeDisabled(); })
CodeSandbox
Uploaded by N2T