Jest를 사용한 단위 테스트
Jest는 JavaScript 프로젝트를 위한 강력한 테스팅 프레임워크로, TypeScript와 함께 사용하면 타입 안전성과 테스트 신뢰성을 높일 수 있습니다.
이 절에서는 Jest와 TypeScript를 통합하여 효과적인 단위 테스트를 작성하는 방법을 살펴봅니다.
Jest와 TypeScript 설정
- 필요한 패키지 설치
npm install --save-dev jest @types/jest ts-jest typescript
- Jest 설정 파일 생성
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
- TypeScript 설정
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"types": ["jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
기본 단위 테스트 작성
함수 테스트 예시
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// math.test.ts
import { add } from './math';
describe('add function', () => {
it('should add two numbers correctly', () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
});
클래스 테스트 예시
// user.ts
export class User {
constructor(public name: string, public age: number) {}
isAdult(): boolean {
return this.age >= 18;
}
}
// user.test.ts
import { User } from './user';
describe('User class', () => {
it('should create a user with name and age', () => {
const user = new User('John', 25);
expect(user.name).toBe('John');
expect(user.age).toBe(25);
});
it('should correctly determine if a user is an adult', () => {
const adult = new User('Jane', 20);
const child = new User('Tom', 15);
expect(adult.isAdult()).toBe(true);
expect(child.isAdult()).toBe(false);
});
});
목(Mock) 사용하기
TypeScript에서 목 사용 예시
// api.ts
export async function fetchUser(id: number): Promise<User> {
// 실제 API 호출
}
// userService.ts
import { fetchUser } from './api';
export async function getUserName(id: number): Promise<string> {
const user = await fetchUser(id);
return user.name;
}
// userService.test.ts
import { getUserName } from './userService';
import { fetchUser } from './api';
jest.mock('./api');
describe('getUserName', () => {
it('should return the user name', async () => {
const mockFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;
mockFetchUser.mockResolvedValue({ id: 1, name: 'John Doe' });
const name = await getUserName(1);
expect(name).toBe('John Doe');
expect(mockFetchUser).toHaveBeenCalledWith(1);
});
});
비동기 코드 테스트
Promise와 async/await 테스트
// asyncService.ts
export async function fetchData(): Promise<string> {
return new Promise(resolve => setTimeout(() => resolve('data'), 1000));
}
// asyncService.test.ts
import { fetchData } from './asyncService';
describe('fetchData', () => {
it('should return data after 1 second', async () => {
const data = await fetchData();
expect(data).toBe('data');
});
it('should return data (using done callback)', (done) => {
fetchData().then(data => {
expect(data).toBe('data');
done();
});
});
});
스냅샷 테스팅
TypeScript 컴포넌트의 스냅샷 테스트
// button.tsx
import React from 'react';
interface ButtonProps {
text: string;
onClick: () => void;
}
export const Button: React.FC<ButtonProps> = ({ text, onClick }) => (
<button onClick={onClick}>{text}</button>
);
// button.test.tsx
import React from 'react';
import renderer from 'react-test-renderer';
import { Button } from './button';
describe('Button component', () => {
it('should render correctly', () => {
const tree = renderer.create(
<Button text="Click me" onClick={() => {}} />
).toJSON();
expect(tree).toMatchSnapshot();
});
});
테스트 커버리지
커버리지 측정 및 보고서 생성
- Jest 설정 파일에 커버리지 옵션 추가
module.exports = {
// ... 기존 설정
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
coverageReporters: ['text', 'lcov'],
};
- 커버리지 리포트 생성
npx jest --coverage
TypeScript 특유의 커버리지 이슈 해결
ts-jest
의astTransformers
옵션을 사용하여 타입 정보를 제거하고 정확한 커버리지 측정
타입 정보를 활용한 테스트 케이스 생성
타입 기반 테스트 케이스 생성 예시
type UserRole = 'admin' | 'user' | 'guest';
function getUserPermissions(role: UserRole): string[] {
// 실제 구현
}
// 타입을 활용한 테스트 케이스 생성
describe('getUserPermissions', () => {
const roles: UserRole[] = ['admin', 'user', 'guest'];
roles.forEach(role => {
it(`should return permissions for ${role}`, () => {
const permissions = getUserPermissions(role);
expect(Array.isArray(permissions)).toBe(true);
// 역할별 특정 검증 로직 추가
});
});
});
Jest 설정 최적화
TypeScript 프로젝트를 위한 Jest 설정 최적화
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
},
},
};
Best Practices와 전략
1. 타입 정의 테스트 : 인터페이스와 타입 정의의 정확성을 검증하는 테스트 작성
2. 제네릭 활용 : 제네릭 함수와 클래스에 대한 다양한 타입 시나리오 테스트
3. 타입 가드 테스트 : 사용자 정의 타입 가드 함수의 정확성 검증
4. 목 타입 안전성 : jest.MockedFunction<T>
와 같은 타입을 활용하여 목의 타입 안전성 보장
5. 비동기 코드 주의사항
async/await
를 사용하여 가독성 높은 비동기 테스트 작성Promise
rejection 테스트 시expect.assertions()
를 사용하여 assertion 횟수 확인
6. 스냅샷 테스트 전략
- UI 컴포넌트의 렌더링 결과를 스냅샷으로 저장
- 스냅샷 업데이트 시 신중하게 검토
7. 테스트 구조화
describe
와it
을 사용하여 테스트를 논리적으로 그룹화beforeEach
와afterEach
를 활용하여 테스트 환경 설정 및 정리
8. 커버리지 목표 설정
- 프로젝트의 중요도에 따라 적절한 커버리지 목표 설정 (예 : 80% 이상)
- 핵심 비즈니스 로직에 대해서는 높은 커버리지 유지
9. 지속적 통합(CI) 파이프라인 구축
- 커밋마다 자동으로 테스트 실행 및 커버리지 리포트 생성
- 커버리지 임계값을 설정하여 품질 관리
10. 문서화
- 각 테스트 케이스의 목적과 테스트하는 시나리오를 명확히 설명
- 복잡한 설정이 필요한 테스트의 경우 주석으로 상세 설명 추가