본문으로 건너뛰기
Advertisement

16.2 Jest + ts-jest — 기존 프로젝트 TypeScript 통합

Jest + TypeScript 설정

npm install --save-dev jest ts-jest @types/jest

jest.config.ts

import type { Config } from 'jest'

const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.{test,spec}.ts'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: {
strict: true,
},
}],
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // 경로 별칭 매핑
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: { lines: 80, functions: 80 },
},
}

export default config

@types/jest와 타입 안전성

// @types/jest를 설치하면 아래 타입들이 자동으로 추론됨
import { describe, test, expect, jest, beforeEach, afterAll } from '@jest/globals'

// 또는 globals: true 설정 시 import 없이 사용 가능
describe('타입 안전 테스트', () => {
test('jest.fn() 타입 추론', () => {
// jest.fn<ReturnType, Args>()
const mockFn = jest.fn<string, [number, string]>()
mockFn.mockReturnValue('result')
mockFn.mockImplementation((n, s) => `${n}-${s}`)

const result = mockFn(1, 'hello')
expect(result).toBe('1-hello')
})

test('jest.spyOn 타입 추론', () => {
const obj = {
greet(name: string): string {
return `Hello, ${name}!`
},
}

const spy = jest.spyOn(obj, 'greet')
spy.mockReturnValue('Mocked!')

expect(obj.greet('Alice')).toBe('Mocked!')
expect(spy).toHaveBeenCalledWith('Alice')
})
})

모듈 자동 모킹

// services/payment.service.ts
export class PaymentService {
async charge(amount: number, cardToken: string): Promise<{ transactionId: string }> {
// 실제 결제 API 호출
const response = await fetch('/api/charge', {
method: 'POST',
body: JSON.stringify({ amount, cardToken }),
})
return response.json()
}
}
// services/order.service.ts
import { PaymentService } from './payment.service'

export class OrderService {
constructor(private paymentService: PaymentService) {}

async createOrder(items: Array<{ price: number }>, cardToken: string) {
const total = items.reduce((sum, item) => sum + item.price, 0)
const { transactionId } = await this.paymentService.charge(total, cardToken)
return {
items,
total,
transactionId,
createdAt: new Date(),
}
}
}
// services/order.service.test.ts
import { OrderService } from './order.service'
import { PaymentService } from './payment.service'

// 자동 모킹
jest.mock('./payment.service')

const MockedPaymentService = jest.mocked(PaymentService)

describe('OrderService', () => {
let orderService: OrderService
let mockPaymentService: jest.Mocked<PaymentService>

beforeEach(() => {
MockedPaymentService.mockClear()
mockPaymentService = new MockedPaymentService() as jest.Mocked<PaymentService>
mockPaymentService.charge.mockResolvedValue({ transactionId: 'txn-123' })
orderService = new OrderService(mockPaymentService)
})

test('주문 생성 시 결제를 처리한다', async () => {
const items = [{ price: 1000 }, { price: 2000 }]
const order = await orderService.createOrder(items, 'card-token')

expect(order.total).toBe(3000)
expect(order.transactionId).toBe('txn-123')
expect(mockPaymentService.charge).toHaveBeenCalledWith(3000, 'card-token')
})
})

커스텀 Matcher 타입 확장

// jest.d.ts
import 'jest'

declare global {
namespace jest {
interface Matchers<R> {
toBeValidEmail(): R
toBeWithinRange(min: number, max: number): R
}
}
}

// jest.setup.ts
expect.extend({
toBeValidEmail(received: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const pass = emailRegex.test(received)
return {
pass,
message: () =>
pass
? `${received}는 유효한 이메일이 아니어야 합니다.`
: `${received}는 유효한 이메일이어야 합니다.`,
}
},

toBeWithinRange(received: number, min: number, max: number) {
const pass = received >= min && received <= max
return {
pass,
message: () =>
`${received}${min}~${max} 범위 ${pass ? '밖' : '안'}에 있어야 합니다.`,
}
},
})

// 사용
test('커스텀 매처', () => {
expect('alice@example.com').toBeValidEmail()
expect(50).toBeWithinRange(0, 100)
})

비동기 테스트 패턴

describe('비동기 테스트', () => {
// 1. async/await
test('async/await', async () => {
const result = await fetchData()
expect(result).toBeDefined()
})

// 2. Promise 반환
test('Promise 반환', () => {
return fetchData().then(result => {
expect(result).toBeDefined()
})
})

// 3. resolves/rejects 매처
test('resolves 매처', async () => {
await expect(fetchData()).resolves.toBeDefined()
})

test('rejects 매처', async () => {
await expect(fetchError()).rejects.toThrow('에러 메시지')
await expect(fetchError()).rejects.toBeInstanceOf(Error)
})

// 4. done 콜백 (레거시)
test('done 콜백', (done) => {
fetchData().then(result => {
expect(result).toBeDefined()
done()
})
})
})

고수 팁

jest.config.ts에서 tsconfig 경로 설정

// jest.config.ts
export default {
preset: 'ts-jest',
moduleNameMapper: {
// tsconfig paths와 동기화
'^@/(.*)$': '<rootDir>/src/$1',
'^@auth/(.*)$': '<rootDir>/src/auth/$1',
'^@db/(.*)$': '<rootDir>/src/db/$1',
},
}

Vitest vs Jest 선택 기준

Vitest 선택:
- Vite 기반 프로젝트
- ESM 네이티브
- 빠른 속도 최우선
- 새 프로젝트 시작

Jest 선택:
- 기존 Jest 테스트 코드 보유
- CRA, Next.js 기본 설정
- 방대한 생태계 필요
- CI/CD 기존 파이프라인 유지
Advertisement