React를 TDD로 개발하는 방법을 간단하게 익혀보자
TDD란 무엇인가
만약 계산기를 개발한다면 다음과 같은 절차를 거친다.
- 계산기 프로그램의 계산 결과를 검증하는 테스트를 작성한다.
- 계산기를 개발한다.
- 계산기가 테스트를 통과하는지 확인한다.
- 테스트 코드를 리팩터링한다.
- 프로그램을 리팩토링한다.
- 테스트가 통과하는지 확인한다.
- 1~6을 반복한다.
테스트 도구 없이 Javascript 코드로 TDD 입문
위에서 언급한 계산기 코드를 직접 짜보자.
1. 테스트를 작성한다.
function additionCalculatorTester() {
if (additionCalculator(4, 6) === 10) {
console.log("✔ Test Passed");
} else {
console.error("❌ Test Failed");
}
}
참고로 다음과 같이 짤 수도 있다.
이 경우 어설션을 통과하지 못하면 오류가 발생한다.
function additionCalculatorTester() {
console.assert(additionCalculator(4, 6) === 10);
}
2. 프로그램을 개발한다.
function additionCalculator(a, b) {
return a + b;
}
3. 테스트를 실행한다.
테스트를 실행하여 계산기의 테스트 통과여부를 확인한다.
additionCalculatorTester();
4. 테스트를 리팩터링한다.
function additionCalculatorTester() {
additionCalculator(4, 6) === 10
? console.log("✔ Test Passed")
: console.error("❌ Test Failed");
}
5. 프로그램을 리팩터링한다.
화살표 함수를 사용하도록 프로그램 코드를 리팩터링한다.
const additionCalculator = (a, b) => a + b;
6. 테스트를 실행한다.
다시 한 번 테스트를 실행하여 계산기의 테스트 통과여부를 확인한다.
additionCalculatorTester();
지금까지 테스트 도구 없이 테스트를 작성하는 방법을 알아보았으나,
Jest와 같은 도구를 사용하면 더 쉽고 효율적으로 테스트를 실행할 수 있다.
Jest 설정 방법
1. 적절한 Node, NPM 버전을 설치한다.
시스템에 Node 10.16(이상) 및 NPM 5.6(이상)이 설치되어 있는지 확인한다.
Node.js 웹사이트에서 최신 LTS를 설치하면 동시에 깔린다.
yarn을 선호하면 Yarn 0.25 이상의 버전을 설치한다.
2. 프로젝트 디렉터리를 만든다.
mkdir addition-calculator-jest-project
3. 프로젝트 디렉터리로 이동한다.
cd path/to/addition-calculator-jest-project
4. package.json 파일을 만든다.
npm init -y
yarn을 사용할 경우 다음과 같이 한다.
yarn init -y
5. Jest를 설치한다.
npm install jest --save-dev
yarn add jest --dev
6. Jest를 테스트 러너로 설정한다.
{
"scripts": {
"test": "jest"
}
}
7. Project 파일을 생성한다.
touch additionCalculator.js
8. 테스트 파일을 작성한다.
touch additionCalculator.test.js
9. 테스트 코드를 작성한다.
// additionCalculator.test.js
const additionCalculator = require("./additionCalculator");
test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});
위 코드의 설명은 다음과 같다.
- addedCalculator.js 파일을 additionCalculator.test.js 테스트 파일로 임포트한다.
- 사용자가 인수로 4와 6을 제공할 때마다 additionCalculator() 프로그램이 10을 출력할 것으로 예상하는 테스트 케이스를 작성한다.
참고
- test()는 Jest의 전역 메서드 중 하나로 다음 세 가지 인수를 허용한다.
- 테스트 명 ex("addition of 4 and 6 to equal 10")
- 출력으로 기대하는 값을 포함한 함수
- 옵셔널한 timeout 값
- expect()는 코드의 출력을 테스트 하기 위한 제스트 메서드이다.
- toBe()는 expect의 인수를 원시 값(primitive value)와 비교하는 Jest matcher다.
10. 프로그램을 개발한다.
미리 작성한 테스트를 통과하는 프로그램을 개발한다.
// additionCalculator.js
function additionCalculator(a, b) {
return a + b;
}
module.exports = additionCalculator;
addedCalculator() 프로그램을 생성하고 module.exports 문으로 export 했다.
11. 테스트를 실행한다.
npm run test
// or
yarn test
특정 파일만 실행하고 싶으면, 다음과 같이 테스트 파일을 지정한다.
npm run test additionCalculator.test.js
yarn test additionCalculator.test.js
$ jest
PASS ./additionCalculator.test.js
√ addition of 4 and 6 to equal 10 (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.002 s
Ran all test suites.
Done in 7.80s.
{
"scripts": {
"test": "jest --watchAll"
}
}
--watchAll을 추가하면 코드의 변경 사항을 저장할 때마다 Jest가 자동으로 테스트를 재실행한다.
12. 테스트 코드 리팩터링
테스트에 요구사항을 추가할 수 있다.
// additionCalculator.test.js
const additionCalculator = require("./additionCalculator");
describe("additionCalculator's test cases", () => {
test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});
test("addition of 100, 50, 20, 45 and 30 to equal 245", () => {
expect(additionCalculator(100, 50, 20, 45, 30)).toBe(245);
});
test("addition of 7 to equal 7", () => {
expect(additionCalculator(7)).toBe(7);
});
test("addition of no argument provided to equal 0", () => {
expect(additionCalculator()).toBe(0);
});
});
describe()는 테스트 코드를 그룹화 하는데 사용한다.
describe()는 두 개의 인수를 받는다.
- "additionCalculator's test cases"와 같은 테스트 그룹 명
- 테스트 케이스를 포함하는 함수
13. 프로그램 리팩터링
// additionCalculator.js
function additionCalculator(...numbers) {
return numbers.reduce((sum, item) => sum + item, 0);
}
module.exports = additionCalculator;
14. 테스트 재실행
테스트를 재실행하여 테스트를 통과하는지 확인한다.
ES6과 Jest 사용 시 주의 사항
Jest는 현재 ES6 모듈을 인식하지 못한다.
하지만 es6의 export, import 문을 사용하고 싶을 수 있다.
이 경우 다음을 수행한다.
1. Babel을 개발 의존성으로 설치
npm install @babel/preset-env --save-dev
// or
yarn add @babel/preset-env --dev
2. project root에 .babelrc 파일 생성
touch .babelrc
3. .babelrc 파일에 다음 코드 복제
{ "presets": ["@babel/preset-env"] }
const additionCalculator = require("./additionCalculator");
module.exports = additionCalculator;
각각 아래처럼 변경할 수 있다.
import additionCalculator from "./additionCalculator";
export default additionCalculator;
4. 테스트 재실행
리팩토링을 적용했으니 테스트를 실행한다.
TDD의 이점
1. 프로그램의 목적을 쉽게 이해할 수 있다.
실제 프로그램을 작성하기 전에 테스트를 작성하기 때문에
TDD는 프로그램의 목적에 대해 생각하게 한다.
하나 이상의 테스트를 통해 프로그램의 목적을 문서화 한 후에는
자신있게 프로그램을 작성할 수 있다.
따라서 TDD는 프로그램이 의도한 바를 구체적으로 적어두는 유용한 방법이다.
2. 개발자에게 더 큰 자신감을 준다.
TDD는 일종의 벤치마크와 같은 역할을 한다.
TDD 용어 : 테스트 종류
Unit test
단위 테스트의 주요 목적은 버그를 확인하는 것이 아니다.
단위 테스트의 핵심 목적은 프로그램의 독립적인 부분(단위라고 함)이
다양한 테스트 케이스에서 의도한 대로 동작하는지 확인하는 것이다.
Integration test
통합 테스트는 종속적인 프로그램의 기능을 평가한다.
즉, 통합 테스트는 다른 코드에 의존하는 프로그램이 의도한 대로 동작하는지 확인한다.
예를 들어 아래 코드는 JS 네이티브 모듈의 reduce 메서드에 의존하고 있기에 통합 테스트라 볼 수도 있다.
만약 JS에서 해당 메서드가 없어진다면 아래 코드는 깨진다.
즉 우리는 reduce()함수와 additionalCalculator()의 통합을 평가하고 있다.
(물론 해당 메서드가 없어질 가능성은 없기 때문에 그냥 unit으로 봐도 된다.)
// additionCalculator.js
function additionCalculator(...numbers) {
return numbers.reduce((sum, item) => sum + item, 0);
}
module.exports = additionCalculator;
End-to-End Test
E2E(End-to-End) 테스트는 사용자 인터페이스의 기능을 평가한다.
즉, E2E는 사용자 인터페이스가 의도한 대로 작동하는지 확인한다.
e2e는 TDD에 끼워넣기 애매한 것 같다.
Max's YouTube video 영상으로 공부한다.
TDD 용어 : 테스트 더블(Test Doubles)
테스트 더블은 데이터베이스, 라이브러리, 네트워크 및 API와 같은 실제 의존성을 모방하는 데 사용되는 가짜 객체다.
테스트 더블은 프로그램의 의존성과 상관없이 코드를 테스트하기 위해 사용한다.
예를 들어 앱의 오류가 외부 API 또는 코드에서 발생했다고 가정하자.
- 외부 코드가 고쳐서 앱이 정상적으로 돌아갈 때까지 기다린다.
- 의존성과 상관없이 동작할 수 있는 우리 코드를 테스트한다. 우리 코드에 문제가 없으면 의존성을 교체한다.
테스트 더블은 테스트가 멈추지 않도록 프로그램의 의존성을 흉내내는 방법이다.
dummy
인수 없이 특정 값을 제공하는 의존성을 모방하는 테스트 더블이다. (ex 리턴값)
()=>({a:1})
mock
리턴값을 제공하지 않는 의존성을 모방하는 테스트 더블이다.
예를 들어 개발 환경에서 접근 불가능하며, 리턴값이 없는 서드파티 API를 모방한다.
(a)=>{}
stub
파라미터에 따라 다른 리턴 값을 리턴하는 의존성을 모방하는 테스트 더블이다.
따라서 스텁은 다양한 응답 시나리오로 프로그램의 동작을 평가하는 데 도움이 된다.
(a)=>(a? 1 : 2)
fake
의존성 프로그램의 동작을 모방하는 테스트 더블이다.
예를 들어 데이터베이스, S3와 같은 실제 소프트웨어 자체를 흉내낸다.
리액트 컴포넌트 테스트 준비
리액트 컴포넌트를 테스트하려면 다음 두 가지 도구가 필요하다.
- 테스트 러너
- 리액트 컴포넌트 테스트 도구
(추가로 알아볼 것 : https://www.daleseo.com/react-hooks-testing-library/)
테스트 러너와 리액트 컴포넌트 테스트 도구의 차이점
- 테스트 러너는 개발자가 테스트 스크립트를 실행하고 테스트 결과를 명령줄(CLI)에 인쇄하는 데 사용하는 도구
- 리액트 컴포넌트 테스트 도구는 컴포넌트를 테스트 하는데 유용한 api를 제공하는 도구
- Enzyme and the React Testing Library.
리액트 컴포넌트 테스트
Jest와 React Testing Library를 사용한다.
1. 적절한 버전의 node와 NPM을 설치한다.
2. CRP 프로젝트를 만든다.
yarn create react-app react-testing-project
3. 프로젝트 디렉터리로 들어간다.
cd react-testing-project
4. 테스트 환경을 구축한다.
아래 네 가지 패키지를 설치한다. CRA는 이미 깔려있다.
- jest
- @testing-library/react
- @testing-library/jest-dom
- @testing-library/user-event
라이브러리 설명
- jest는 이 프로젝트의 테스트 스크립트를 실행하고 테스트 결과를 명령줄에 인쇄하는 데 사용할 테스트 러너 도구다.
- @testing-library/react는 React 컴포넌트 테스트 케이스 작성에 필요한 API를 제공하는 React Testing Library다.
- @testing-library/jest-dom은 DOM의 상태를 테스트하기 위한 커스텀 Jest 매처 세트를 제공한다.
- 참고: Jest는 이미 많은 매처와 함께 제공되므로 jest-dom을 사용하는 것은 선택 사항이다.
- jest-dom은 테스트를 더 선언적이고 읽기 쉽고 유지보수하기 쉽게 해주는 api를 제공한다.
- @testing-library/user-event는 웹 페이지에서 앱과 사용자의 상호 작용을 시뮬레이션하기 위한 userEvent API를 제공한다.
- fireEvent보다 낫다.
5. src 폴더를 정리한다.
6. src 폴더에 아래 파일을 만든다.
- index.js
- App.js
- App.test.js
7. App 컴포넌트를 렌더링한다.
// index.js
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
// Render the App component into the root DOM
createRoot(document.getElementById("root")).render(<App />);
8. 테스트 케이스를 작성한다.
목표는 App이 <h1>CodeSweetly Test</h1>를 렌더링 하는 것이다.
// App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import App from "./App";
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
});
- 테스트 케이스를 작성하는 데 필요한 패키지를 임포트한다.
- <App /> 컴포넌트가 "codesweetly test" 텍스트로 h1 엘리먼트를 렌더링 할 것을 예상하는 코드를 작성한다.
- render()는 React testing library의 API로 원하는 컴포넌트 렌더링 시 사용한다.
- expect()는 코드 출력을 테스트 하는데 사용한다.
- screen은 페이지에서 엘리먼트를 찾는 데 사용한다.
- getByRole()는 페이지에서 엘리먼트를 찾기 위한 React Testing Library의 쿼리 메서드 중 하나다.
- toHaveTextContent()는 텍스트 콘텐츠가 있는지 확인하는 데 사용하는 jest-dom의 커스텀 매처 중 하나다.
// 1. Using jest-dom's toHaveTextContent() method:
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
// 2. Using the heading's textContent property and Jest's toMatch() method:
expect(screen.getByRole("heading").textContent).toMatch(/codesweetly test/i);
// 3. Using React Testing Library's name option and jest-dom's toBeInTheDocument() method
expect(screen.getByRole("heading", { name: /codesweetly test/i })).toBeInTheDocument();
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(/codesweetly test/i);
});
9. 컴포넌트를 개발한다.
// App.js
import React from "react";
const App = () => <h1>CodeSweetly Test</h1>;
export default App;
10. 테스트를 실행한다.
yarn test App.test.js
다음과 같은 콘솔 메세지를 확인할 수 있다.
$ jest
PASS src/App.test.js
√ codesweetly test heading (59 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.146 s
Ran all test suites related to changed files.
create-react-app은 디폴트로 jest를 watch 모드로 실행하므로, 다른 명령어를 수행하려면 다른 창을 띄운다.
11. 앱을 실행한다.
yarn start
12. 테스트를 리팩터링한다.
사용자가 버튼을 클릭하면 제목의 텍스트가 바뀌도록 하고 싶다.
즉, 상호작용을 테스트하고 싶다.
// App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import App from "./App";
describe("App component", () => {
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
});
test("a codesweetly project heading", () => {
render(<App />);
const button = screen.getByRole("button", { name: "Update Heading" });
userEvent.click(button);
expect(screen.getByRole("heading")).toHaveTextContent(/a codesweetly project/i);
});
});
- userEvent는 사용자와 앱의 상호 작용 시뮬레이션을 위한 여러 메서드가 포함된 React Testing Library 패키지다.
- 예를 들어 위의 스니펫에서는 userEvent의 click() 메서드를 사용하여 버튼 요소의 클릭 이벤트를 시뮬레이션한다.
13. 컴포넌트를 리팩터링한다.
버튼을 클릭하여 컴포넌트 텍스트를 변경하는 기능을 추가한다.
// App.js
import React, { useState } from "react";
const App = () => {
const [heading, setHeading] = useState("CodeSweetly Test");
const handleClick = () => {
setHeading("A CodeSweetly Project");
};
return (
<>
<h1>{heading}</h1>
<button type="button" onClick={handleClick}>
Update Heading
</button>
</>
);
};
export default App;
14. 테스트를 다시 실행한다.
더 좋은 테스트를 작성하는 방법, 클린한 테스트 코드를 작성하는 법 등 다양한 주제가 남았지만,
이 정도면 기초는 충실하게 다룬것 같다.
'FrontEnd' 카테고리의 다른 글
[JS] 한글을 2byte로 한 줄 길이 제한 및 줄바꿈 코드 (0) | 2023.03.24 |
---|---|
[Vite, Vitest, Javascript] Vanilla JS로 TDD with Vitest (0) | 2023.03.23 |
[React] Context API를 활용한 전략 패턴 (0) | 2023.03.19 |
[번역] 서버에서 Redux 사용하기 (0) | 2023.03.15 |
[Redux] 리덕스의 state, action, reducer (0) | 2023.03.14 |