단위 테스트 설정
품질 좋은 소프트웨어를 개발하는 데 있어 테스팅(Testing) 은 필수적인 과정입니다. 특히, 애플리케이션의 가장 작은 단위인 함수나 컴포넌트를 개별적으로 검증하는 단위 테스트(Unit Testing) 는 코드의 안정성을 높이고 버그를 조기에 발견하며, 코드 리팩터링 시 안전망 역할을 합니다. Next.js 프로젝트에서 단위 테스트를 설정하고 실행하는 데 가장 널리 사용되는 도구는 Jest입니다.
이 절에서는 단위 테스트의 중요성, Jest를 Next.js 프로젝트에 설정하는 방법, 기본적인 Jest 사용법, 그리고 Next.js 환경에서 컴포넌트 테스트를 위한 @testing-library/react
와의 연동에 대해 상세히 알아보겠습니다.
단위 테스트의 중요성
- 버그 조기 발견: 개발 초기 단계에서 코드의 작은 부분에 숨어 있는 버그를 신속하게 식별하고 수정할 수 있습니다. 이는 통합 테스트나 실제 배포 후에 버그를 발견하는 것보다 훨씬 비용 효율적입니다.
- 코드 품질 향상: 테스트 가능한 코드를 작성하려면 모듈화와 재사용성이 높은 코드를 설계해야 합니다. 이는 자연스럽게 코드의 응집도를 높이고 결합도를 낮추어 전반적인 코드 품질을 향상시킵니다.
- 리팩터링의 안전망: 기존 코드를 개선하거나 변경할 때, 단위 테스트가 있다면 변경 사항이 기존 기능에 영향을 미치지 않는지 빠르게 확인할 수 있습니다.
- 개발 생산성 향상: 테스트를 통해 변경 사항의 영향을 즉시 확인할 수 있으므로, 개발자가 자신감을 가지고 코드를 수정하고 새로운 기능을 추가할 수 있습니다.
- 문서화: 잘 작성된 단위 테스트는 코드의 기능과 예상되는 동작에 대한 살아있는 문서 역할을 합니다.
Jest란?
Jest는 Facebook에서 개발한 JavaScript 테스팅 프레임워크로, React 애플리케이션 테스트에 특히 인기가 많습니다. Jest는 'Zero-configuration'을 지향하며, 테스트 러너, 어설션 라이브러리, Mocking 도구 등을 모두 내장하고 있어 별도의 추가 설정 없이 바로 테스트를 시작할 수 있다는 장점이 있습니다.
Next.js 프로젝트에 Jest 설정하기
Next.js는 Jest를 위한 통합된 설정을 제공하므로, 설정 과정이 비교적 간단합니다.
필요한 패키지 설치
먼저 Jest와 React 컴포넌트 테스트에 필요한 @testing-library/react
및 관련 패키지들을 설치합니다.
npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
# 또는
yarn add --dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
jest
: Jest 테스팅 프레임워크 본체.@testing-library/react
: React 컴포넌트를 테스트하기 위한 유틸리티. 사용자가 컴포넌트와 상호작용하는 방식에 가깝게 테스트를 작성할 수 있도록 돕습니다.@testing-library/jest-dom
: Jest에 DOM 관련 매처(matcher)를 추가하여 DOM 요소를 더 쉽게 테스트할 수 있게 합니다.jest-environment-jsdom
: Jest가 브라우저 환경을 시뮬레이션할 수 있도록 하는 환경. React 컴포넌트 테스트에 필수적입니다.
Jest 설정 파일 생성
프로젝트 루트에 jest.config.js
파일을 생성합니다. Next.js는 Jest 설정에 대한 가이드를 제공하며, 이를 기반으로 설정할 수 있습니다.
# 프로젝트 루트 디렉토리
your-nextjs-app/
├── jest.config.js # 여기에 파일 생성
├── package.json
└── ...
jest.config.js
예시
// jest.config.js
const nextJest = require('next/jest');
// Next.js Jest 설정 유틸리티를 초기화합니다.
const createJestConfig = nextJest({
// Next.js 애플리케이션의 루트 디렉토리를 제공합니다.
dir: './',
});
// Jest에 추가적인 커스텀 설정을 추가할 수 있습니다.
const customJestConfig = {
// 테스트 환경을 jsdom으로 설정하여 DOM 환경을 시뮬레이션합니다.
testEnvironment: 'jsdom',
// 테스트 파일의 패턴을 정의합니다. (e.g., .test.js, .spec.js)
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$',
// 모듈 해석 방식을 설정합니다.
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
// 테스트 전 전역적으로 실행될 setup 파일을 지정합니다.
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
// 특정 경로를 모듈로 인식하도록 합니다. Next.js의 path alias와 유사.
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
// CSS 모듈에 대한 mock 처리 (Jest가 CSS 파일을 직접 해석하지 못하므로)
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
// 테스트 커버리지 보고서를 생성할지 여부를 설정합니다.
collectCoverage: false,
// 테스트 커버리지 보고서를 생성할 파일 패턴을 지정합니다.
collectCoverageFrom: [
'components/**/*.{js,jsx,ts,tsx}',
'app/**/*.{js,jsx,ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
],
};
// Next.js Jest 설정과 커스텀 설정을 병합하여 내보냅니다.
module.exports = createJestConfig(customJestConfig);
Jest Setup 파일 생성
jest.config.js
에서 지정한 setupFilesAfterEnv
경로에 jest.setup.js
파일을 생성합니다. 이 파일은 각 테스트가 실행되기 전에 전역적으로 실행될 코드를 포함합니다. 주로 @testing-library/jest-dom
을 임포트하여 DOM 매처를 확장하는 데 사용됩니다.
# 프로젝트 루트 디렉토리
your-nextjs-app/
├── jest.setup.js # 여기에 파일 생성
└── ...
jest.setup.js
예시:
// jest.setup.js
import '@testing-library/jest-dom/extend-expect';
// 여기서는 @testing-library/jest-dom의 확장된 매처를 임포트합니다.
// 예: toBeInTheDocument(), toHaveClass() 등을 사용할 수 있게 됩니다.
package.json
에 테스트 스크립트 추가
package.json
파일의 scripts
섹션에 Jest 실행 명령어를 추가합니다.
// package.json
{
"name": "your-nextjs-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest", // Jest 실행 스크립트
"test:watch": "jest --watch" // 파일 변경 시 자동 재실행
},
// ... (dependencies, devDependencies)
}
이제 npm test
또는 yarn test
명령어로 Jest를 실행할 수 있습니다.
기본적인 단위 테스트 작성하기
함수와 컴포넌트 단위 테스트의 예시를 살펴보겠습니다.
순수 함수 테스트
가장 간단한 형태의 단위 테스트입니다. 입력에 대해 항상 동일한 출력을 반환하는 순수 함수를 테스트합니다.
// lib/utils.ts
export function sum(a: number, b: number): number {
return a + b;
}
export function capitalize(str: string): string {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
// __tests__/utils.test.ts
import { sum, capitalize } from '../lib/utils'; // 테스트할 함수 임포트
describe('Utils 함수 테스트', () => {
test('sum 함수는 두 숫자의 합을 반환해야 합니다.', () => {
expect(sum(1, 2)).toBe(3);
expect(sum(-1, 1)).toBe(0);
expect(sum(0, 0)).toBe(0);
});
test('capitalize 함수는 문자열의 첫 글자를 대문자로 만들어야 합니다.', () => {
expect(capitalize('hello')).toBe('Hello');
expect(capitalize('world')).toBe('World');
expect(capitalize('')).toBe('');
expect(capitalize('a')).toBe('A');
});
});
React 컴포넌트 테스트
@testing-library/react
는 컴포넌트의 내부 구현보다는 사용자 관점에서 컴포넌트가 어떻게 동작하는지를 테스트하도록 권장합니다.
// components/Button.tsx
"use client"; // 클라이언트 컴포넌트
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export default function Button({ children, onClick, disabled = false }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
style={{
padding: '10px 20px',
backgroundColor: disabled ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>
{children}
</button>
);
}
// __tests__/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; // 필요한 유틸리티 임포트
import Button from '../components/Button'; // 테스트할 컴포넌트 임포트
describe('Button 컴포넌트', () => {
test('버튼 텍스트가 올바르게 렌더링되어야 합니다.', () => {
render(<Button>클릭하세요</Button>); // 컴포넌트 렌더링
// screen.getByText를 사용하여 화면에 "클릭하세요" 텍스트가 있는지 확인
expect(screen.getByText('클릭하세요')).toBeInTheDocument();
});
test('버튼 클릭 시 onClick 핸들러가 호출되어야 합니다.', () => {
const handleClick = jest.fn(); // Jest의 Mock 함수 생성
render(<Button onClick={handleClick}>Submit</Button>);
// 화면에서 "Submit" 텍스트를 가진 버튼을 찾고 클릭 이벤트를 발생시킵니다.
fireEvent.click(screen.getByText('Submit'));
// handleClick Mock 함수가 호출되었는지 확인합니다.
expect(handleClick).toHaveBeenCalledTimes(1); // 1번 호출되었는지
});
test('disabled prop이 true일 때 버튼이 비활성화되어야 합니다.', () => {
render(<Button disabled>비활성화 버튼</Button>);
// "비활성화 버튼" 텍스트를 가진 버튼이 disabled 상태인지 확인
expect(screen.getByText('비활성화 버튼')).toBeDisabled();
});
test('disabled prop이 false일 때 버튼이 활성화되어야 합니다.', () => {
render(<Button disabled={false}>활성화 버튼</Button>);
// "활성화 버튼" 텍스트를 가진 버튼이 enabled 상태인지 확인
expect(screen.getByText('활성화 버튼')).not.toBeDisabled();
});
});
Mocking (모킹)
실제 서비스에서 API 호출, 데이터베이스 접근 등 외부 의존성이 있는 코드를 테스트할 때는 모킹(Mocking) 기법을 사용합니다. Jest는 강력한 모킹 기능을 제공하여 외부 의존성 없이 단위 테스트를 독립적으로 실행할 수 있도록 돕습니다.
// lib/api.ts (실제 API 호출)
export async function fetchUserData(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}`);
if (!res.ok) {
throw new Error('사용자 데이터를 가져오지 못했습니다.');
}
return res.json();
}
// __tests__/api.test.ts
import { fetchUserData } from '../lib/api';
// fetch 함수를 Mocking
// Jest는 전역 fetch를 자동으로 Mocking 해주지는 않습니다.
// 여기서는 Node.js 환경에서 fetch를 Mocking하기 위한 예시입니다.
// 실제 Next.js 앱에서는 Jest 설정에서 'whatwg-fetch' 또는 'node-fetch'를 Mocking할 수 있습니다.
// 혹은 MSW (Mock Service Worker) 같은 라이브러리를 사용하는 것이 더 강력합니다.
describe('API 함수 테스트', () => {
// 테스트 시작 전에 fetch를 Mocking합니다.
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: '123', name: 'Test User' }),
} as Response)
) as jest.Mock;
});
// 각 테스트 후 Mocking을 초기화합니다.
afterEach(() => {
jest.restoreAllMocks();
});
test('fetchUserData는 사용자 데이터를 성공적으로 가져와야 합니다.', async () => {
const user = await fetchUserData('123');
expect(user).toEqual({ id: '123', name: 'Test User' });
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/users/123');
});
test('fetchUserData는 오류 발생 시 예외를 던져야 합니다.', async () => {
// 오류 상황을 시뮬레이션하기 위해 fetch Mocking을 재정의
global.fetch = jest.fn(() =>
Promise.resolve({
ok: false,
status: 404,
json: () => Promise.resolve({ message: 'Not Found' }),
} as Response)
) as jest.Mock;
await expect(fetchUserData('nonexistent')).rejects.toThrow('사용자 데이터를 가져오지 못했습니다.');
});
});
Jest 사용 시 고려사항 및 팁
- 테스트 파일 위치: 일반적으로
src
디렉토리와 동일한 레벨에__tests__
디렉토리를 만들거나, 테스트할 파일과 같은 디렉토리에.test.ts
또는.spec.ts
접미사를 붙여 파일을 생성합니다. - 테스트 범위: 모든 코드를 100% 테스트할 필요는 없습니다. 비즈니스 로직의 핵심 부분, 복잡한 로직, 버그가 자주 발생하는 부분 등을 우선적으로 테스트합니다.
- 테스트 코드와 제품 코드 분리: 테스트 코드는 제품 코드와 함께 배포되지 않도록
devDependencies
로 관리합니다. - 간결하고 명확한 테스트: 각 테스트는 하나의 특정 시나리오만 검증하도록 간결하게 작성합니다. 테스트의 목적과 예상 결과를 명확하게 나타내도록 테스트 이름을 짓습니다.
- TDD (Test-Driven Development): 테스트 주도 개발은 코드를 작성하기 전에 테스트를 먼저 작성하는 방식입니다. 이는 코드 설계와 품질을 향상시키는 데 도움이 될 수 있습니다.
- 스냅샷 테스트 (Snapshot Testing): UI 컴포넌트의 변경 사항을 추적하는 데 유용합니다. 컴포넌트가 렌더링된 결과를 스냅샷으로 저장하고, 이후 테스트 실행 시 저장된 스냅샷과 현재 렌더링 결과를 비교합니다. (남용은 피하는 것이 좋습니다.)
Jest를 Next.js 프로젝트에 통합하고 단위 테스트를 작성하는 것은 애플리케이션의 신뢰성과 유지보수성을 크게 향상시키는 중요한 단계입니다. 효과적인 테스트 전략을 통해 더 견고한 웹 애플리케이션을 구축할 수 있습니다.