TDD(Test Driven Development: 테스트 주도 개발)
TDD는 개발자가 새로운 기능이나 함수에 대한 테스트 케이스를 먼저 작성하고, 그 테스트를 통과하기 위한 최소한의 코드를 작성하는 개발 기법이다. 이 방법론은 코드의 기능에 초점을 맞추며, 개발자의 관점에서 테스트 케이스를 작성한다.
- 실패하는 테스트 작성: 구현할 기능에 대한 테스트 케이스를 작성하여 초기 상태에서 테스트가 실패하도록 만든다.
- 테스트를 통과하도록 수정: 작성한 테스트를 통과시키기 위해 필요한 최소한의 코드를 구현한다.
- 리팩토링: 통과한 테스트를 기반으로 코드의 품질을 개선하고 가독성을 높인다.
BDD(Behavior Driven Development: 행동 주도 개발)
BDD는 TDD에서 파생된 개발 방법론으로, 테스트 케이스 자체가 요구 사항이 되도록 하는 개발 방법을 말한다.
이는 사용자의 관점에서 시스템의 행동에 초점을 맞추어 테스트 케이스를 작성한다.
BDD의 구조
- Given: 주어진 환경이나 조건을 설정한다.
- 이 단계에서는 테스트를 수행하기 전에 필요한 초기 상태를 정의한다.
- When: 특정 행위를 수행한다.
- 사용자가 어떤 행동을 취하는지를 설명하는 단계이다.
- Then: 기대하는 결과를 검증한다.
- 이 단계에서는 사용자가 수행한 행위에 대한 결과를 확인한다.
1. Jest
📌 Jest란?
Jest는 페이스북(Facebook)에서 개발한 JavaScript 테스트 프레임워크이다.
특히 React와 호환성이 뛰어나며, Babel, TypeScript, Node.js, Angular, Vue 등 다양한 프로젝트에서 활용이 가능하다.
이를 통해 단위 테스트(Unit Test), 통합 테스트(Integration Test), 스냅샷 테스트(Snapshot Test) 등을 수행할 수 있다.
📌 Jest를 사용하는 이유는 무엇일까
Jest가 등장하기 이전에는 여러 자바스크립트 테스팅 라이브러리를 조합해서 사용해야 했다.
Test Runner - Mocha, Jasmin
Test Matcher - Chai, Expect
Test Mock - Sinon, Testdouble
Jest는 이러한 모든 기능을 하나의 통합된 테스트 프레임워크로 제공한다!
📌 주요 특징
- 간편한 설정과 빠른 실행 속도
Jest는 초기 설정이 거의 필요 없으며, 기본 설정만으로도 다양한 테스트 시나리오를 지원한다.
또한, 테스트를 병렬로 실행하여 빠른 피드백을 제공한다. - 스냅샷 테스팅
UI 컴포넌트의 렌더링 결과를 스냅샷으로 저장하고, 이후 변경 사항과 비교하여 의도하지 않은 변경을 쉽게 감지할 수 있다. - 테스트 커버리지 리포트
코드 커버리지 리포트를 생성하여 테스트가 코드의 어느 부분을 커버하고 있는지 파악할 수 있다. - 자동화된 테스트 실행과 상세한 결과 제공
Jest는 자동으로 테스트를 실행하고, 실패한 테스트에 대해 상세한 오류 메시지와 함께 컨텍스트를 제공하여 문제를 신속하게 해결할 수 있게 해준다.
📌 설치 방법
1. Jest 설치: yarn add --dev jest 명령어로 Jest 설치
2. package.json 수정
// package.json
{
"scripts": {
"test": "jest"
}
}
3. 테스트 실행: yarn test 명령어로 Jest 테스트 실행
📌 테스트 파일 인식 기준
Jest는 특정 패턴을 가진 파일을 자동으로 테스트 파일로 인식하여 실행한다.
- 파일명에 .test.js 또는 .spec.js가 포함된 경우
- __tests__ 폴더 내에 있는 파일
- tests 폴더 안에 있는 파일
예를 들어, add.test.js, add.spec.js, __tests__/add.js와 같은 파일이 테스트 파일로 인식된다.
📌 Jest의 테스트 코드 작성 방식
Jest를 사용하여 테스트를 작성하는 방식에는 여러 가지가 있다.
1️⃣ 개별 테스트 나열 방식
- test : 단일 테스트 케이스 작성
test('1과 2를 더하면 3이 된다', () => { // 테스트에 대한 설명
expect(add(1, 2)).toBe(3); // 예상값과 실제값 비교
});
2️⃣ BDD 스타일 테스트 조직화
BDD 스타일은 describe와 it 함수를 사용하여 테스트를 더 구조화하고 가독성을 높이는 방식이다.
- describe : 테스트의 큰 범주 정의, 기능 단위의 테스트를 진행할 때 관련 테스트를 그룹화
describe('add 함수 테스트', () => {
it('1과 2를 더하면 3이 된다', () => {
expect(add(1, 2)).toBe(3);
});
it('음수를 더하면 올바른 값을 반환한다', () => {
expect(add(-1, -2)).toBe(-3);
});
it('0을 더하면 다른 수가 그대로 반환된다', () => {
expect(add(0, 5)).toBe(5);
});
});
it 함수와 test 함수는 동일한 기능을 한다. 따라서, 위 코드에서 it 대신 test를 사용해도 괜찮다.
📌 Jest의 Matchers
Matcher는 테스트의 예상 결과를 정의하는 데 사용되는 함수이다.
Jest에서 expect 함수와 함께 사용되어 실제 값이 예상한 값과 일치하는지 검증한다.
- toBe: 기본적인 동등성 검사 (원시 값 비교, 동일 객체 참조 확인)
expect(add(1, 2)).toBe(3);
- toEqual: 객체나 배열의 내용 동등성 검사
const obj = { a: 1, b: 2 };
expect(obj).toEqual({ a: 1, b: 2 })
- toBeNull, toBeUndefined, toBeTruthy, toBeFalsy: 특정 값 상태 검사
const value = null;
expect(value).toBeNull()
- toContain: 배열이나 문자열에 특정 값 포함 여부 검사
const fruits = ['apple', 'banana', 'cherry'];
expect(fruits).toContain('banana');
- toMatch: 정규식이나 문자열 패턴 매칭 검사
const email = 'test@example.com';
expect(email).toMatch(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/);
📌 Jest의 테스트 커버리지
테스트 커버리지(Test Coverage)는 소프트웨어 테스트에서 테스트가 코드의 어느 부분을 실행하고 검증하는지를 측정하는 지표 중 하나이다. 이를 통해 개발자는 테스트가 코드의 모든 중요한 부분을 제대로 검증하고 있는지, 또는 어떤 부분이 테스트되지 않아 잠재적인 버그가 존재할 가능성이 있는지를 파악할 수 있다.
"scripts" : {
"coverage": "jest --coverage"
}
yarn coverage를 통해 커버리지 확인❗️
커버리지가 높다면 코드의 대부분 또는 모든 부분이 테스트에 의해 검증되었음을 의미하기 때문에
소프트웨어의 안정성과 신뢰성이 높다고 볼 수 있다.
📌 Jest의 Mock 기능
모킹(Mock)은 단위 테스트에서 특정 함수나 모듈을 실제 구현 대신 가짜(mock)로 대체하여 외부 의존성을 분리한 채 특정 동작을 시뮬레이션할 수 있는 기능이다.
함수 모킹
jest.fn()을 사용하여 함수 모킹
이 모킹된 함수는 호출 여부나 호출된 횟수, 호출된 인자 등을 추적할 수 있게 해준다.
const mockFn = jest.fn(); // mockFn을 가짜 함수로 설정
mockFn(); // 함수를 호출
expect(mockFn).toHaveBeenCalled(); // mockFn이 호출되었는지 검사
모듈 모킹
jest.mock()을 사용하여 외부 모듈 모킹
axios를 모킹하여 외부 API 호출을 테스트
axios.get.mockResolvedValue를 사용하여 axios.get 호출 시 반환될 가짜 데이터를 설정
실제로 API를 호출하지 않고, 가짜 데이터를 이용해 테스트를 진행하는 방식
jest.mock('axios');
import axios from 'axios';
test('데이터를 가져와야 한다', async () => {
axios.get.mockResolvedValue({ data: { user: 'Alice' } });
const data = await fetchData();
expect(data.user).toBe('Alice');
});
2. React Testing Library
React Testing Library는 React 컴포넌트의 테스트를 보다 쉽고 직관적으로 작성할 수 있도록 도와주는 라이브러리이다. RTL은 행위 주도 테스트(Behavior Driven Testing) 방식을 채택하여, 사용자의 실제 상호작용을 기반으로 컴포넌트를 테스트한다. 즉, 컴포넌트의 내부 구현보다는 사용자 경험을 중심으로 테스트를 진행한다.
📌 RTL의 주요 특징
- 사용자 중심의 테스트
실제 사용자가 애플리케이션을 사용하는 방식과 유사하게 테스트를 작성한다.
- 내부 구현에 대한 의존성 감소
DOM 요소의 구조나 클래스 이름보다는 사용자에게 보여지는 결과에 집중한다.
- Jest와의 호환성
RTL과 Jest는 상호 보완적인 관계이다. Jest는 기본적인 테스트 인프라와 API를 제공하고, RTL은 React 컴포넌트를 사용자 관점에서 테스트하기 위한 추가 도구를 제공하므로, 이 둘을 함께 사용하는 것이 보편적이다.
📌 설치 및 설정
Create React App(CRA)을 사용하는 경우 기본적으로 RTL이 내장되어 있어 별도의 설정이 필요 없다.
Create React App을 사용하지 않는 경우, RTL을 설치하려면 다음 명령어를 실행해야 한다.
yarn add --dev @testing-library/react @testing-library/jest-dom
설치 후, Jest 설정 파일에 @testing-library/jest-dom을 추가하여 추가적인 매처를 사용할 수 있도록 설정해야 한다.
// setupTests.js
import '@testing-library/jest-dom';
📌 기본 사용법
RTL을 사용하여 리액트 컴포넌트를 테스트하는 기본적인 방법을 살펴보자.
import React from 'react';
const Hello = ({ name }: { name: string }) => {
return <div>Hello {name}</div>;
};
export default Hello;
위 컴포넌트를 테스트하는 코드는 다음과 같다.
import React from 'react';
import { render, screen } from '@testing-library/react';
import Hello from './Hello';
test('renders hello message', () => {
render(<Hello name="World" />);
const helloElement = screen.getByText(/Hello World/i);
expect(helloElement).toBeInTheDocument();
});
- render: 테스트할 컴포넌트 렌더링
- screen.getByText: 화면에 특정 텍스트가 존재하는지 확인
- expect(...).toBeInTheDocument(): 해당 요소가 DOM에 존재함을 검증
💡 screen 객체
screen 객체는 렌더링된 DOM에 접근할 수 있는 주요 방법을 제공한다.
아래와 같은 다양한 쿼리 메서드를 사용하여 요소를 가져올 수 있다.
- getByText
- getByRole
- getByLabelText
- getByPlaceholderText
- getByTestId 등
📌 주요 함수와 활용 예제
RTL의 다양한 유틸리티 함수를 통해 사용자 상호작용을 시뮬레이션하고 컴포넌트를 테스트할 수 있다.
사용자 상호작용을 시뮬레이션할 때 활용되는 주요 함수와 테스트 코드를 알아보자!
1️⃣ fireEvent
fireEvent는 간단한 사용자 이벤트를 시뮬레이션하는 함수이다.
다음은 입력 창에 값을 입력하는 테스트 코드이다.
// InputComponent.tsx
import React, { useState } from 'react';
const InputComponent = () => {
const [value, setValue] = useState('');
return (
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p>{value}</p>
</div>
);
};
export default InputComponent;
// InputComponent.test.ts
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import InputComponent from './InputComponent';
test('updates value on input change', () => {
render(<InputComponent />);
const input = screen.getByLabelText(/name/i); // 레이블 텍스트 "name"과 연결된 <input> 요소
fireEvent.change(input, { target: { value: 'New Name' } });
// fireEvent.change는 요소의 변경 이벤트를 발생시킨다.
expect(screen.getByText('New Name')).toBeInTheDocument();
});
- getByLabelText는 레이블과 폼 요소가 htmlFor 속성(또는 래핑)으로 올바르게 연결되어 있을 때만 작동한다.
2️⃣ userEvent
userEvent는 fireEvent보다 더 실제 사용자와 유사한 이벤트를 시뮬레이션할 수 있는 유틸리티이다.
fireEvent와는 달리 비동기 작업을 포함한 복잡한 상호작용을 처리할 수 있다.
다음은 버튼을 클릭하는 테스트 코드이다.
// ButtonComponent.tsx
import React, { useState } from 'react';
const ButtonComponent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>클릭!</button>
<p>{count}번 클릭</p>
</div>
);
};
export default ButtonComponent;
// ButtonComponent.test.ts
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ButtonComponent from './ButtonComponent';
test('버튼 클릭 시 카운트가 증가합니다', async () => {
render(<ButtonComponent />);
const button = screen.getByText(/클릭!/i);
await userEvent.click(button);
// userEvent.click은 사용자가 특정 요소(예: 버튼)를 클릭하는 행동을 나타낸다.
expect(screen.getByText(/1번 클릭/i)).toBeInTheDocument();
});
'리액트 심화 스터디' 카테고리의 다른 글
🏄♀️ 리심스 7주차 - Concurrent Mode (1) | 2024.12.11 |
---|---|
[Week 7] Concurrent Mode, Suspense (2) | 2024.12.10 |
[Week 6] React Profiler, React.lazy, Debounce, Throttle (2) | 2024.12.03 |
🏄♀️ 리심스 6주차 - 리액트 성능 최적화 (2) | 2024.12.03 |
🏄♀️ 리심스 5주차 - 리액트 서버 컴포넌트 (2) | 2024.11.26 |