Jest로 비즈니스 로직 단위 테스트하기
Node.js 환경에서 비즈니스 로직을 Jest로 단위 테스트하는 방법과 테스트 설계, mocking, 비동기 처리 예제까지 정리한 실무형 학습 자료
목차
개요
단위 테스트는 코드의 작은 단위가 의도대로 동작하는지 검증하는 과정이다. 특히 비즈니스 로직은 외부 의존성을 최소화하고 핵심 동작을 검증해야 한다. 본문에서는 Node.js 환경에서 Jest를 사용해 비즈니스 로직을 설계하고 테스트하는 방법을 사례 중심으로 설명한다.
테스트 설계의 기본 원칙
무엇을 테스트할지 결정
비즈니스 로직은 계산, 상태 전환, 규칙 적용 등 핵심 책임을 가진다. 테스트 대상은 다음과 같다.
- 입력에 따른 결과값 검증
- 에러 처리와 예외 케이스
- 외부 의존성 호출 횟수와 파라미터
- 비동기 흐름의 성공·실패 경로
독립성 유지
단위 테스트는 외부 리소스(데이터베이스, 네트워크)에 의존하면 안 된다. 대신 mocking을 통해 의존성을 격리한다. 이렇게 해야 테스트가 빠르고 안정적이다.
Jest 설치 및 기본 설정
프로젝트 루트에서 Jest를 설치한다. package.json에 테스트 스크립트를 등록하면 실행이 편리하다.
npm install --save-dev jest
# package.json 예시
{
'scripts': {
'test': 'jest'
}
}
간단한 비즈니스 로직 예제
예제로 주문 금액에 할인 규칙을 적용하는 service를 만든다. 외부 결제 API 호출은 분리해서 mocking을 적용한다.
// orderService.js
function calculateTotal(items, discountRate) {
const subtotal = items.reduce((s, i) => s + i.price * i.quantity, 0);
const discount = Math.max(0, Math.min(discountRate, 1));
return +(subtotal * (1 - discount)).toFixed(2);
}
async function processPayment(paymentClient, order) {
if (!order.items || order.items.length === 0) throw new Error('NO_ITEMS');
const total = calculateTotal(order.items, order.discountRate || 0);
const res = await paymentClient.charge({ amount: total, currency: 'USD' });
if (!res.success) throw new Error('PAYMENT_FAILED');
return { orderId: order.id, paid: true, amount: total };
}
module.exports = { calculateTotal, processPayment };
테스트 코드 작성
순수 함수 테스트
calculateTotal은 순수 함수이므로 다양한 입력 케이스로 검증한다. 소숫점 반올림과 할인 경계도 확인한다.
// orderService.test.js
const { calculateTotal } = require('./orderService');
test('단순 합산과 할인 적용', () => {
const items = [{ price: 10, quantity: 2 }, { price: 5, quantity: 1 }];
expect(calculateTotal(items, 0.1)).toBe(22.5);
});
test('할인율 경계 처리', () => {
const items = [{ price: 100, quantity: 1 }];
expect(calculateTotal(items, 2)).toBe(0); // 2는 100% 이상이므로 0 처리
});
외부 의존성 mocking
processPayment는 외부 결제 클라이언트를 사용한다. 실제 결제를 호출하면 안 되므로 jest.fn()이나 jest.mock()으로 동작을 흉내 낸다.
// orderService.test.js 계속
const { processPayment } = require('./orderService');
test('결제 성공 흐름', async () => {
const fakeClient = { charge: jest.fn(async () => ({ success: true })) };
const order = { id: 'o1', items: [{ price: 50, quantity: 1 }], discountRate: 0 };
const result = await processPayment(fakeClient, order);
expect(fakeClient.charge).toHaveBeenCalledWith({ amount: 50, currency: 'USD' });
expect(result).toEqual({ orderId: 'o1', paid: true, amount: 50 });
});
test('결제 실패 예외 처리', async () => {
const fakeClient = { charge: jest.fn(async () => ({ success: false })) };
const order = { id: 'o2', items: [{ price: 20, quantity: 2 }] };
await expect(processPayment(fakeClient, order)).rejects.toThrow('PAYMENT_FAILED');
});
비동기 테스트와 예외 케이스
비동기 함수는 async/await와 jest의 rejects, resolves를 함께 쓰면 명확하게 검증할 수 있다. 또한 입력 검증 로직도 별도로 확인한다.
// orderService.test.js 계속
test('아이템 없을 때 예외 발생', async () => {
const fakeClient = { charge: jest.fn() };
const order = { id: 'o3', items: [] };
await expect(processPayment(fakeClient, order)).rejects.toThrow('NO_ITEMS');
});
Mock을 활용한 더 복잡한 시나리오
의존성이 많은 함수는 다양한 응답을 시뮬레이션해야 한다. 네트워크 지연, 재시도 로직, 실패 후 복구 등을 테스트에 포함한다.
- 함수 호출 횟수 검증: toHaveBeenCalledTimes
- 호출 인자 검증: toHaveBeenCalledWith
- 타이머 사용 시 jest.useFakeTimers로 시간 제어
테스트 유지보수 팁
- 테스트는 읽기 쉽고 의도가 분명해야 한다.
- 한 테스트는 한 가지 동작만 검증한다.
- 비즈니스 규칙이 변경되면 테스트를 먼저 업데이트한다(리팩터링 안전성 확보).
결론
Jest는 간결한 API로 Node.js 비즈니스 로직 테스트에 적합하다. 순수 함수는 다양한 입력으로 검증하고, 외부 의존성은 mocking으로 격리한다. 비동기 흐름과 예외 처리까지 테스트하면 운영 환경에서의 신뢰도를 높일 수 있다. 위 예제를 바탕으로 실제 서비스 로직에 맞춰 테스트 범위를 점진적으로 확장하면 좋다.