Jest는 테스트 실행기 + 검증 도구
React Testing Library (RTL)는 React 컴포넌트 렌더링 & DOM 쿼리 도구
Jest는 언제 사용하는가?
비즈니스 로직 / 유틸 함수 / API / 상태 변경을 테스트할 때
// utils/calc.ts
export const sum = (a: number, b: number) => a + b;
test('sum 함수 테스트', () => {
expect(sum(2, 3)).toBe(5); // ✅ Jest만으로 충분
});
사용하는 기능
- test(), describe(), expect()
- jest.fn(), mockReturnValue()
- beforeEach(), afterAll()
- resolves, rejects (비동기 처리)
컴포넌트가 아닌 일반 함수/비동기 로직은 Jest만으로 테스트 가능합니다.
React Testing Library는 언제 사용하는가?
React 컴포넌트의 렌더링, 사용자 상호작용, UI 반응을 테스트할 때
render(<Button label="클릭" onClick={fn} />);
const button = screen.getByText("클릭");
fireEvent.click(button); // ✅ 사용자가 클릭하는 행위를 시뮬레이션
사용하는 기능
- render() : 컴포넌트를 가상 DOM에 렌더링
- screen.getByText(), getByRole() 등 쿼리 함수
- fireEvent, userEvent로 이벤트 발생
- waitFor, findByText()로 비동기 테스트
컴포넌트를 테스트할 땐 반드시 React Testing Library가 필요
대신 검증(expect)은 Jest를 사용 → 그래서 항상 두 도구를 같이 쓰는 것처럼 보이는 것
함수, 유틸 로직 | Jest 단독 | sum(), formatDate() 등 |
API 호출 테스트 (with MSW) | Jest + mock 도구 | fetchUser() 등 |
컴포넌트 렌더링 | Jest + RTL | render(<MyComponent />) |
UI에 특정 텍스트가 있는지 | Jest + RTL | getByText("로그인") |
버튼 클릭 이벤트 처리 | Jest + RTL | fireEvent.click(button) |
비동기 렌더링 확인 | Jest + RTL + findByText | 로딩 후 "완료" 메시지 확인 등 |
// Jest + RTL 조합
test('버튼 클릭 시 텍스트 변경', () => {
render(<MyButton />);
fireEvent.click(screen.getByText('클릭')); // RTL
expect(screen.getByText('완료')).toBeInTheDocument(); // Jest + RTL
});
1. Jest의 역할
Test Runner | .test.js 또는 .spec.js 파일을 찾아 실행 |
Assertion Library | expect() 구문으로 결과를 검증 |
Mocking 기능 | 의존성을 가짜로 만들어 테스트 가능하게 함 |
2. 기본 사용 구조
test('설명', () => {
// 1. 준비 (Arrange)
// 2. 실행 (Act)
// 3. 검증 (Assert)
});
예시:
test('1 + 2는 3이다', () => {
const result = 1 + 2;
expect(result).toBe(3);
});
Jest 폴더
__폴더명__(예: __tests__, __mocks__, __snapshots__)처럼 언더스코어(__)로 감싼 폴더명은 특별한 의미가 있는 관례적인 구조
__tests__ | ❌ 선택 | 코드와 테스트를 분리하고 싶을 때 |
__mocks__ | 🔶 조건부 | Jest의 자동 mock 기능을 쓸 경우 |
__snapshots__ | ✅ 자동 생성 | 스냅샷 테스트를 사용할 경우에만 |
→ 반드시 써야 하는 건 아니고, 구조적 정리가 필요할 때 선택적으로 사용
3. 자주 쓰는 Jest 메서드 & 기능
test 또는 it
test('설명', () => {...});
it('설명', () => {...}); // 동일함
expect()
expect(실제값).matcher(예상값) 형태로 작성
.toBe(value) | === 값 비교 |
.toEqual(obj) | 객체 값 비교 (deep comparison) |
.toContain(item) | 배열 또는 문자열 포함 여부 |
.toHaveLength(n) | 길이 검사 |
.toBeTruthy() / .toBeFalsy() | 참/거짓 여부 |
.toBeNull() / .toBeUndefined() | null 또는 undefined 여부 |
.toBeGreaterThan(n) | 숫자 비교 |
.toMatch(regex) | 문자열 정규표현식 비교 |
.toHaveBeenCalled() | mock 함수 호출 여부 |
.toHaveBeenCalledTimes(n) | 몇 번 호출됐는지 |
.toHaveBeenCalledWith(arg) | 어떤 인자로 호출됐는지 |
4. 구조화 도우미
describe()
- 관련 테스트들을 그룹화
describe('덧셈 함수', () => {
test('1 + 1 = 2', () => { ... });
test('0 + 0 = 0', () => { ... });
});
beforeEach / afterEach
- 매 테스트 전후에 공통 동작 실행
beforeEach(() => {
console.log('매 테스트마다 실행됨');
});
beforeAll / afterAll
- 전체 테스트 한 번만 실행
beforeAll(() => {
console.log('테스트 시작 전 한 번 실행');
});
5. Mock 관련 메서드
jest.fn()
- 가짜 함수(mock 함수)를 생성
const mockFn = jest.fn();
mockFn('hello');
expect(mockFn).toHaveBeenCalledWith('hello');
jest.mock()
- 외부 모듈 전체를 mock 처리
jest.mock('../api/userAPI'); // userAPI 모듈을 자동으로 mock 처리
mockReturnValue()
const mock = jest.fn().mockReturnValue(42);
expect(mock()).toBe(42);
mockResolvedValue() / mockRejectedValue()
- 비동기 함수의 반환값도 설정 가능
const fetchUser = jest.fn().mockResolvedValue({ name: 'mars112' });
const failAPI = jest.fn().mockRejectedValue(new Error('실패!'));
6. 비동기 테스트
1) async/await 사용
test('비동기 함수가 값을 반환해야 한다', async () => {
const fetchData = () => Promise.resolve(42);
const result = await fetchData();
expect(result).toBe(42);
});
2) .resolves / .rejects
test('resolves matcher 사용', () => {
return expect(Promise.resolve('OK')).resolves.toBe('OK');
});
test('rejects matcher 사용', () => {
return expect(Promise.reject('에러')).rejects.toBe('에러');
});
유틸 함수 테스트
// utils/math.ts
export const add = (a: number, b: number) => a + b;
// math.test.ts
import { add } from './math';
test('add는 두 수를 더한다', () => {
expect(add(2, 3)).toBe(5);
});
컴포넌트 테스트 (React Testing Library 사용)
// Button.tsx
export const Button = ({ onClick }: { onClick: () => void }) => (
<button onClick={onClick}>클릭</button>
);
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
test('버튼 클릭 시 함수가 호출됨', () => {
const onClick = jest.fn();
render(<Button onClick={onClick} />);
fireEvent.click(screen.getByText('클릭'));
expect(onClick).toHaveBeenCalledTimes(1);
});
8. Jest 설정 파일 (jest.config.ts)
export default {
testEnvironment: 'jsdom', // 브라우저 환경
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // 절대경로 import 지원
},
};
실무에 바로 쓸 수 있게 정리
1. setupTests.ts 구성
테스트 환경을 초기화하는 전역 설정 파일입니다.
보통 @testing-library/jest-dom 같은 확장 matcher를 불러오거나, 공통 mock을 여기에 정의해요.
파일 위치
src/
├── setupTests.ts ← 여기에 설정
예시
// src/setupTests.ts
import '@testing-library/jest-dom'; // toBeInTheDocument 등 확장 matcher 지원
jest.config.ts에 등록 필요
// jest.config.ts
export default {
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};
2. Mock Server 연동 테스트 (MSW 사용)
**Mock Service Worker (MSW)**를 사용하면 fetch, axios 요청을 가짜 서버로 처리 가능
실제 API가 없어도 테스트 가능하고, 비동기 테스트에서 매우 유용합니다.
설치
npm install msw --save-dev
구조
src/
├── mocks/
│ ├── handlers.ts
│ └── server.ts
├── setupTests.ts
handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ name: 'mars112' }));
}),
];
server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
setupTests.ts
import '@testing-library/jest-dom';
import { server } from './mocks/server';
// 테스트 전체 전/후 훅 등록
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
테스트
import { render, screen } from '@testing-library/react';
import { useEffect, useState } from 'react';
const User = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
return <div>{user ? user.name : '로딩 중...'}</div>;
};
test('유저 데이터를 렌더링한다', async () => {
render(<User />);
const name = await screen.findByText('mars112');
expect(name).toBeInTheDocument();
});
3. Jest로 TDD
TDD(Test-Driven Development): “테스트 먼저 쓰고 → 코드를 작성해서 통과시킴”
이메일 유효성 검사 함수
Step 1: 테스트부터 작성 (email.test.ts)
import { validateEmail } from './email'; // 아직 없음
test('올바른 이메일은 통과해야 한다', () => {
expect(validateEmail('abc@example.com')).toBe(true);
});
test('잘못된 이메일은 실패해야 한다', () => {
expect(validateEmail('hello@wrong')).toBe(false);
expect(validateEmail('no-at-symbol.com')).toBe(false);
});
테스트 실패 (validateEmail이 아직 없음)
Step 2: 함수 생성 & 테스트 통과시키기 (email.ts)
export const validateEmail = (email: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
npm test → 통과됨!
jest test code 작성 순서
- 실패하는 테스트 작성
- 테스트를 통과할 최소한의 코드 작성
- 통과 후 리팩토링
- 기능 안정성과 의도 확인 가능
1. React Testing Library(RTL)의 역할
렌더링 도구 | render()로 컴포넌트를 가상 DOM에 마운트 |
쿼리 함수 | 실제 사용자처럼 텍스트/레이블 등을 기준으로 DOM 요소 찾기 |
이벤트 시뮬레이션 | fireEvent, userEvent로 클릭/입력/포커스 등 실행 |
비동기 처리 지원 | findBy, waitFor로 비동기 렌더링 대응 가능 |
2. 기본 사용 구조
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('설명', () => {
render(<컴포넌트 />);
const 요소 = screen.getByText('텍스트');
userEvent.click(요소);
expect(요소).toBeInTheDocument();
});
3. 자주 쓰는 메서드 & 쿼리 함수
render()
- 컴포넌트를 테스트 환경에서 가상 DOM에 마운트
render(<MyComponent />);
screen의 쿼리 함수들
getByText() | 텍스트로 요소 찾기 (하나만 있을 때) |
getByRole() | 역할(버튼, heading 등)으로 찾기 |
getByLabelText() | <label>과 연결된 input 찾기 |
getByPlaceholderText() | placeholder로 찾기 |
getByTestId() | data-testid 속성으로 찾기 |
queryBy... | 요소가 없을 수도 있을 때 |
findBy... | 비동기적으로 기다렸다가 찾기 (Promise) |
expect(screen.getByText("제목")).toBeInTheDocument();
이벤트 도구
fireEvent
- 기본 DOM 이벤트 (예: click, change)
fireEvent.click(screen.getByText('클릭'));
userEvent
- 실제 사용자 행동에 가까운 고급 시뮬레이션
await userEvent.type(input, 'Hello');
await userEvent.click(button);
userEvent는 실제로 input에 포커스 → 타이핑 → blur 순으로 작동합니다.
4. 비동기 처리
test('로딩 후 데이터 표시', async () => {
render(<UserProfile />);
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
const name = await screen.findByText('mars112');
expect(name).toBeInTheDocument();
});
추가 유틸: waitFor()
await waitFor(() => {
expect(screen.getByText('완료')).toBeInTheDocument();
});
입력폼 테스트
test('입력값이 반영되는가', async () => {
render(<input placeholder="이름 입력" />);
const input = screen.getByPlaceholderText('이름 입력');
await userEvent.type(input, 'mars112');
expect(input).toHaveValue('mars112');
});
버튼 클릭 이벤트
test('버튼 클릭 시 텍스트 변경', async () => {
render(<Button />);
const btn = screen.getByText('시작');
await userEvent.click(btn);
expect(screen.getByText('완료')).toBeInTheDocument();
});
6. 테스트용 속성 - data-testid
CSS class, id 같은 스타일 속성이 아니라, 테스트 전용으로 쓰이는 속성
<div data-testid="user-name">mars112</div>
const name = screen.getByTestId('user-name');
expect(name).toHaveTextContent('mars112');
가능하면 getByText, getByRole 등 사용자 중심 접근 우선 사용 → getByTestId는 최후의 수단
7. 전역 설정 (setupTests.ts)
// setupTests.ts
import '@testing-library/jest-dom'; // 확장 matcher 제공 (toBeInTheDocument 등)
등록 위치 (jest.config.ts)
export default {
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};
8. 확장 matcher (@testing-library/jest-dom)
expect(element).toBeInTheDocument();
expect(button).toBeDisabled();
expect(input).toHaveValue('mars112');
expect(link).toHaveAttribute('href', '/home');
이런 matcher는 jest-dom을 통해 사용!
1. 비동기 컴포넌트 테스트 실전 예제
컴포넌트: User.tsx
import { useEffect, useState } from 'react';
export const User = () => {
const [name, setName] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setName(data.name);
setLoading(false);
});
}, []);
if (loading) return <p>로딩 중...</p>;
return <h1>안녕하세요, {name}님</h1>;
};
테스트 코드: User.test.tsx (with MSW)
import { render, screen } from '@testing-library/react';
import { User } from './User';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
// Mock server
const server = setupServer(
rest.get('/api/user', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ name: 'mars112' }))
)
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('사용자 데이터를 로딩 후 표시해야 함', async () => {
render(<User />);
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
const name = await screen.findByText('안녕하세요, mars112님');
expect(name).toBeInTheDocument();
});
2. Form Validation 테스트
컴포넌트: LoginForm.tsx
import { useState } from 'react';
export const LoginForm = () => {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email.includes('@')) {
setError('올바른 이메일을 입력하세요');
} else {
setError('');
alert('로그인 성공');
}
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="이메일"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">로그인</button>
{error && <p>{error}</p>}
</form>
);
};
테스트 코드: LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
test('올바르지 않은 이메일 입력 시 오류 메시지 출력', async () => {
render(<LoginForm />);
const input = screen.getByPlaceholderText('이메일');
const button = screen.getByText('로그인');
await userEvent.type(input, 'wrong-email');
await userEvent.click(button);
expect(screen.getByText('올바른 이메일을 입력하세요')).toBeInTheDocument();
});
3. Mocking Axios 요청 테스트
컴포넌트: PostList.tsx
import { useEffect, useState } from 'react';
import axios from 'axios';
export const PostList = () => {
const [posts, setPosts] = useState<string[]>([]);
useEffect(() => {
axios.get('/api/posts').then((res) => setPosts(res.data));
}, []);
return (
<ul>
{posts.map((title, i) => (
<li key={i}>{title}</li>
))}
</ul>
);
};
테스트 코드: PostList.test.tsx
import { render, screen } from '@testing-library/react';
import { PostList } from './PostList';
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
test('axios를 통해 글 목록을 가져와 렌더링', async () => {
mockedAxios.get.mockResolvedValue({ data: ['글1', '글2'] });
render(<PostList />);
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent('글1');
});
'JavaScript > React' 카테고리의 다른 글
React에서 화면 사이즈 감지하기(반응형 설정하기, window.innerWidth, matchMedia, react-responsive, tailwind) (0) | 2025.03.31 |
---|---|
.env (1) | 2025.03.28 |
React 고급 Hook (0) | 2025.03.21 |
useRef, useImperativeHandle (1) | 2025.03.21 |
useEffect, useLayoutEffect, useInsertionEffect (0) | 2025.03.21 |