Jest를 사용한 단위 테스트
소프트웨어 개발에서 테스팅은 필수적인 과정입니다. 코드가 예상대로 동작하는지 확인하고, 잠재적인 버그를 미리 발견하며, 향후 코드 변경 시 기존 기능이 손상되지 않음을 보장하는 데 중요한 역할을 합니다. 특히 현대 웹 애플리케이션 개발에서는 복잡성이 증가함에 따라 체계적인 테스트 전략이 더욱 중요해지고 있습니다.
테스트에는 여러 종류가 있지만, 가장 기본적이고 빈번하게 수행되는 것이 단위 테스트(Unit Test) 입니다. 단위 테스트는 애플리케이션의 가장 작은 독립적인 단위(함수, 클래스 메서드 등)를 격리하여 테스트하는 것을 말합니다. 이 절에서는 자바스크립트 및 타입스크립트 프로젝트에서 널리 사용되는 테스트 프레임워크인 Jest를 사용하여 단위 테스트를 작성하는 방법에 대해 자세히 알아보겠습니다.
단위 테스트의 중요성
단위 테스트는 다음과 같은 이점을 제공합니다.
- 버그 조기 발견: 개발 초기 단계에서 작은 단위의 버그를 발견하여 전체 시스템의 문제로 확산되는 것을 방지합니다.
- 코드 품질 향상: 테스트를 염두에 두고 코드를 작성하면 자연스럽게 모듈화가 잘 되고, 응집도가 높으며, 결합도가 낮은 코드를 작성하게 되어 코드 품질이 향상됩니다.
- 리팩토링 자신감: 코드 리팩토링 시 기존 기능의 오작동을 테스트를 통해 빠르게 파악할 수 있어 변경에 대한 두려움을 줄여줍니다.
- 문서화 역할: 잘 작성된 단위 테스트는 해당 코드 단위가 어떤 기능을 수행하고 어떻게 사용되는지에 대한 실행 가능한 문서 역할을 합니다.
- 개발 생산성 향상: 장기적으로는 버그 수정에 드는 시간과 노력을 줄여 전체적인 개발 생산성을 높입니다.
Jest 소개 및 설정
Jest는 Facebook에서 개발한 자바스크립트 테스트 프레임워크입니다. React 프로젝트에서 특히 많이 사용되지만, 모든 자바스크립트/타입스크립트 프로젝트에서 범용적으로 사용할 수 있습니다.
Jest의 주요 특징
- Zero-configuration (거의 설정 불필요): 대부분의 경우 별도의 설정 없이 바로 사용할 수 있을 정도로 초기 설정이 간편합니다.
- 모든 것이 포함: 테스트 러너, 단언(Assertion) 라이브러리, Mocking 기능이 모두 내장되어 있어 추가적인 라이브러리 설치가 필요 없습니다.
- 빠른 성능: 병렬 테스트 실행을 지원하여 테스트 시간을 단축합니다.
- 스냅샷 테스팅: UI 컴포넌트나 큰 객체의 구조가 예상과 일치하는지 스냅샷을 찍어 비교하는 기능을 제공합니다.
프로젝트 설정
새로운 Node.js 프로젝트에서 Jest와 타입스크립트를 사용하기 위한 설정 방법은 다음과 같습니다.
프로젝트 초기화
mkdir my-jest-ts-app
cd my-jest-ts-app
npm init -y
Jest 및 타입스크립트 관련 의존성 설치
npm install --save-dev jest typescript ts-jest @types/jest @types/node
jest
: Jest 테스트 프레임워크typescript
: 타입스크립트 컴파일러ts-jest
: Jest가 타입스크립트 파일을 처리할 수 있도록 해주는 프리셋@types/jest
: Jest의 전역 변수(describe, test, expect 등)에 대한 타입 정의@types/node
: Node.js 환경에서 사용되는 전역 객체(console 등)에 대한 타입 정의
tsconfig.json
설정:
npx tsc --init
명령어로 기본 tsconfig.json
을 생성한 후, Jest와 관련된 설정을 추가합니다.
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": ["es2018"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
// Jest 관련 설정
"types": ["jest", "node"] // Jest와 Node.js의 타입 정의를 포함
},
"include": ["src/**/*.ts", "src/**/*.test.ts"], // 테스트 파일도 컴파일 범위에 포함
"exclude": ["node_modules"]
}
jest.config.js
설정:
ts-jest
를 사용하기 위한 Jest 설정을 추가합니다. 프로젝트 루트에 jest.config.js
파일을 생성합니다.
module.exports = {
preset: 'ts-jest', // ts-jest 프리셋 사용
testEnvironment: 'node', // 테스트 환경 (Node.js 환경)
// 테스트 파일을 찾을 경로 패턴
testMatch: ["<rootDir>/src/**/*.test.(ts|js)"],
// 모듈 해결을 위한 설정 (별칭 경로 등)
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1" // src 폴더에 대한 @/ 별칭 (옵션)
},
// 테스트 실행 전 전역 설정 파일
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"], // 예: 추가 matcher 등록
};
preset: 'ts-jest'
: Jest가.ts
파일을 컴파일하고 실행할 수 있도록 합니다.testEnvironment: 'node'
: 테스트가 Node.js 환경에서 실행됩니다. (브라우저 환경 테스트는jsdom
등을 사용)testMatch
: Jest가 테스트 파일을 찾을 패턴을 정의합니다. (일반적으로src
폴더 내의.test.ts
파일)
package.json
스크립트 추가:
Jest를 실행하기 위한 스크립트를 추가합니다.
{
"name": "my-jest-ts-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest", // 모든 테스트 실행
"test:watch": "jest --watch", // 파일 변경 감지하며 테스트 실행
"test:cov": "jest --coverage" // 테스트 커버리지 보고서 생성
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.14.10",
"jest": "^29.7.0",
"ts-jest": "^29.1.5",
"typescript": "^5.5.3"
}
}
기본적인 단위 테스트 작성하기
이제 간단한 함수를 만들고 Jest를 사용하여 단위 테스트를 작성해봅시다.
테스트 대상 코드 (src/math.ts
)
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
테스트 코드 (src/math.test.ts
)
Jest는 describe
, test
(또는 it
), expect
, toBe
와 같은 전역 함수들을 제공합니다. 타입스크립트는 이들 함수의 사용법과 인자 타입을 정확히 추론하여 개발을 돕습니다.
import { add, subtract, multiply, divide } from './math'; // 테스트 대상 함수 임포트
// describe 블록: 관련 테스트들을 그룹화합니다.
describe('Math operations', () => {
// test (또는 it) 블록: 개별 테스트 케이스를 정의합니다.
test('add function should correctly add two numbers', () => {
// expect(value): 테스트할 값을 지정합니다.
// .toBe(expected): Jest의 Matcher로, 값이 예상과 같은지 확인합니다.
expect(add(1, 2)).toBe(3);
expect(add(0, 0)).toBe(0);
expect(add(-1, 1)).toBe(0);
expect(add(100, 200)).toBe(300);
});
test('subtract function should correctly subtract two numbers', () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(10, 0)).toBe(10);
expect(subtract(0, 5)).toBe(-5);
expect(subtract(-5, -2)).toBe(-3);
});
test('multiply function should correctly multiply two numbers', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(5, 0)).toBe(0);
expect(multiply(-2, 4)).toBe(-8);
expect(multiply(-3, -3)).toBe(9);
});
test('divide function should correctly divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
expect(divide(7, 2)).toBe(3.5);
expect(divide(0, 5)).toBe(0);
});
// 에러 발생 테스트
test('divide function should throw an error when dividing by zero', () => {
// 특정 함수가 에러를 던지는지 테스트할 때는 expect 안에 함수를 래핑해야 합니다.
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
// 특정 에러 타입인지도 확인할 수 있습니다 (예: .toThrow(TypeError))
expect(() => divide(10, 0)).toThrow(Error);
});
});
테스트 실행
터미널에서 다음 명령어를 실행합니다.
npm test
Jest는 src/math.test.ts
파일을 찾아 테스트를 실행하고 결과를 보고합니다.
Jest의 주요 Matcher
expect
와 함께 사용되는 Matcher는 Jest가 제공하는 다양한 단언 메서드입니다. 몇 가지 자주 사용되는 Matcher는 다음과 같습니다.
-
동등성
toBe(value)
: 원시 값(숫자, 문자열, 불리언 등)의 동등성 비교 (Object.is 사용)toEqual(value)
: 객체나 배열의 깊은 동등성 비교 (재귀적으로 필드 값 비교)toStrictEqual(value)
:toEqual
과 유사하지만, undefined 속성이나 배열 희소성 등 엄격한 동등성 비교
-
진실성
toBeTruthy()
: 값이 논리적으로true
인지 확인 (0, '', null, undefined, false는false
)toBeFalsy()
: 값이 논리적으로false
인지 확인toBeNull()
: 값이null
인지 확인toBeUndefined()
: 값이undefined
인지 확인toBeDefined()
: 값이undefined
가 아닌지 확인
-
숫자
toBeGreaterThan(number)
: ~보다 큰지toBeGreaterThanOrEqual(number)
: ~보다 크거나 같은지toBeLessThan(number)
: ~보다 작은지toBeLessThanOrEqual(number)
: ~보다 작거나 같은지toBeCloseTo(number, precision)
: 부동소수점 오차를 고려하여 근사치 비교
-
문자열
toMatch(regexp | string)
: 문자열이 정규 표현식 또는 부분 문자열과 일치하는지
-
배열/이터러블
toContain(item)
: 배열에 특정 아이템이 포함되어 있는지toContainEqual(item)
: 배열에 특정 객체 아이템이 깊은 동등성으로 포함되어 있는지
-
예외
toThrow(error?)
: 함수가 에러를 던지는지 확인
-
부정
.not
: 모든 Matcher 앞에 붙여 반대 의미로 사용 (expect(value).not.toBe(false)
)
Mocking
단위 테스트에서는 테스트 대상 코드를 격리하여 테스트해야 합니다. 이때 테스트 대상 코드가 의존하는 외부 시스템(데이터베이스, 네트워크 요청, 파일 시스템 등)이나 복잡한 객체는 Mocking (모킹) 을 통해 실제 동작을 흉내 내는 가짜 객체로 대체합니다. Jest는 강력한 Mocking 기능을 내장하고 있습니다.
예시: 비동기 데이터 로딩 함수 Mocking
interface User {
id: number;
name: string;
}
export async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
}
import { fetchUser } from './api';
// Jest의 Mocking 기능을 사용하여 fetch 함수를 모킹합니다.
// global.fetch를 스파이하고 모킹합니다.
beforeAll(() => {
jest.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Test User' }),
} as Response) // Response 타입으로 단언
);
});
afterEach(() => {
// 각 테스트 후 Mock 초기화
jest.clearAllMocks();
});
afterAll(() => {
// 모든 테스트 후 Mock 복원
jest.restoreAllMocks();
});
describe('fetchUser function', () => {
test('should fetch user data successfully', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Test User' });
expect(global.fetch).toHaveBeenCalledTimes(1); // fetch 함수가 한 번 호출되었는지 확인
expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/users/1'); // 정확한 인자로 호출되었는지 확인
});
test('should throw an error if fetch fails', async () => {
// 이번 테스트에서는 fetch가 실패하도록 Mocking을 재정의합니다.
jest.spyOn(global, 'fetch').mockImplementationOnce(() =>
Promise.resolve({
ok: false,
status: 404,
statusText: 'Not Found',
} as Response)
);
await expect(fetchUser(99)).rejects.toThrow('Failed to fetch user');
});
});
jest.spyOn
을 사용하면 실제 함수를 호출하면서 해당 함수의 호출 여부, 인자 등을 추적할 수 있습니다. mockImplementation
이나 mockImplementationOnce
를 사용하여 가짜 구현을 제공할 수 있습니다. 타입스크립트는 이러한 Mocking 과정에서도 함수 시그니처와 인자의 타입을 검사하여 올바른 Mocking이 이루어지도록 돕습니다.
타입스크립트와 Jest의 시너지
Jest는 타입스크립트를 기본적으로 지원하지는 않지만, ts-jest
와 @types/jest
를 통해 완벽하게 통합됩니다. 이는 다음과 같은 이점을 제공합니다.
- 강력한 타입 안전성: 테스트 대상 코드뿐만 아니라 테스트 코드 자체도 타입 검사의 혜택을 받습니다. 잘못된 인자 전달, 오타 등으로 인한 오류를 컴파일 시점에 방지합니다.
- 향상된 개발자 경험: Jest의 전역 함수(
describe
,test
,expect
등) 및 Matcher에 대한 정확한 타입 정의 덕분에 IDE에서 자동 완성, JSDoc 기반 도움말, 실시간 오류 감지 등을 받을 수 있습니다. - 명확한 코드 의도: 테스트 코드의 가독성이 높아지고, 어떤 데이터가 입력되고 어떤 결과가 예상되는지 타입 정의를 통해 명확하게 드러납니다.
결론
단위 테스트는 현대 소프트웨어 개발에서 견고하고 안정적인 애플리케이션을 구축하는 데 있어 필수적인 요소입니다. Jest는 간편한 설정, 풍부한 기능, 빠른 실행 속도를 제공하는 강력한 자바스크립트 테스트 프레임워크입니다. 여기에 타입스크립트를 결합하면, 테스트 코드 자체의 품질과 안정성을 높이고, 개발 과정에서 발생할 수 있는 잠재적인 오류를 효과적으로 줄일 수 있습니다.
단위 테스트를 꾸준히 작성하고 Jest의 다양한 기능을 활용하여 코드의 신뢰도를 높이는 것은 장기적으로 개발 생산성을 향상시키고, 더 나은 소프트웨어 제품을 만드는 기반이 됩니다.