✍🏻/독서록
테스트 코드 현실 적용기: Unit Testing 책을 통한 인사이트와 실무 경험
foodev
2025. 1. 29. 22:19
728x90
Unit Testing 책 리뷰와 실무 적용기
들어가며
"Unit Testing: Principles, Practices, and Patterns"(Vladimir Khorikov 저) 책을 읽고 실무에 적용하면서 느낀 점을 정리해보았습니다. 테스트 코드를 작성하는 여러 방법론과 실제 현장에서의 적용 과정에서 배운 점들을 공유하고자 합니다.
(실은 읽다가 어려워서 누락된 내용도 꽤 많아요 ㅎㅎ이해하고 넘어가주세요)
책의 핵심 내용
테스트의 기본 원칙
- 테스트는 비즈니스 시나리오를 반영해야 함
- 테스트 가독성이 중요
- 테스트가 구현이 아닌 동작을 검증해야 함
단위 테스트의 정의
- 작은 코드조각을 검증하고
- 빠르게 수행하고
- 격리된 방식으로 처리하는 자동화된 테스트
고전파 vs 런던파
격리 문제로 인한 두 학파의 차이
- 고전파: 실제 의존성을 사용
- 런던파: 모든 의존성을 mock으로 대체
// 고전파 예시
test('주문 생성', () => {
// 실제 의존성 사용
const db = new Database();
const payment = new Payment();
const order = new Order(db, payment);
order.create();
expect(db.findOrder()).toBeDefined();
});
// 런던파 예시
test('주문 생성', () => {
// 모든 의존성을 mock으로 대체
const mockDB = { save: jest.fn() };
const mockPayment = { process: jest.fn() };
const order = new Order(mockDB, mockPayment);
order.create();
expect(mockDB.save).toHaveBeenCalled();
});
두 학파의 주요 차이점
- 테스트 주도 개발을 통한 시스템 설계 방식
- 과도한 명세 문제
테스트 주도 개발(TDD)
TDD의 핵심 원칙
- 실패하는 테스트를 먼저 작성
- 코드가 완벽하지 않아도 됨
- 읽기 쉽고 유지보수가 용이하도록 작성
실무 적용 경험
프로젝트 진행 과정
- 기획 완료 후 프론트엔드/백엔드 동시 개발 시작
- 데이터 모델 설계
- Swagger를 통한 API 문서화 및 소통
- 개발 완료 후 API 테스트 및 QA
실무에서 겪은 어려움과 해결책
- Swagger 문서 작성이 TDD로 인해 지연되는 문제 발생
- 해결책: Swagger용 mock 객체 먼저 생성 후 TDD 진행
- 현재는 API 개발 후 AI 도움을 받아 테스트 케이스 작성
테스트 유형별 특징
통합 테스트와 E2E 테스트
- 통합 테스트: 여러 컴포넌트간 상호작용 검증
- E2E 테스트: 전체 시스템 흐름 테스트
테스트 대역(Test Double)의 이해
- 더미(Dummy): 단순 전달용 객체
- 스텁(Stub): 미리 준비된 응답 제공
- 스파이(Spy): 메서드 호출 기록
- 목(Mock): 예상 동작 검증
- 페이크(Fake): 간소화된 실제 구현체
블랙박스/화이트박스 테스트
- 블랙박스: 내부 구조를 모르는 상태에서 테스트
- 화이트박스: 내부 구조를 아는 상태에서 테스트
테스트 피라미드
테스트 코드의 이상적인 구조
단위 테스트 >> 통합 테스트 >> E2E 테스트 순으로 작성 권장
피라미드 구조를 권장하는 이유
- E2E 테스트의 한계
- 높은 유지보수 비용
- 긴 실행 시간
- 불안정성 (깨지기 쉬움)
- 단위 테스트의 장점
- 빠른 실행 속도
- 안정적인 결과
- 문제 발생 지점 특정 용이
테스트 유형별 코드 예시
1. 단위 테스트
// 독립적인 단위 기능 테스트
test('이메일 유효성 검사', () => {
const validator = new EmailValidator();
expect(validator.isValid('test@email.com')).toBe(true);
expect(validator.isValid('invalid-email')).toBe(false);
});
2. 통합 테스트
// 여러 컴포넌트 연동 테스트
test('유저 생성 및 이메일 발송', async () => {
const userService = new UserService(database, emailService);
const user = await userService.createUser({
email: 'test@email.com',
name: 'John'
});
// 실제 DB 저장 및 이메일 발송 검증
const savedUser = await database.findUser(user.id);
expect(savedUser).toBeDefined();
});
3. E2E 테스트
// 전체 시스템 흐름 테스트
test('회원가입 프로세스', async () => {
// 회원가입
const signupRes = await request(app)
.post('/api/signup')
.send({
email: 'test@email.com',
password: 'password123'
});
// 로그인 및 토큰 발급
const loginRes = await request(app)
.post('/api/login')
.send({
email: 'test@email.com',
password: 'password123'
});
// 인증이 필요한 API 호출
const profileRes = await request(app)
.get('/api/profile')
.set('Authorization', loginRes.body.token);
expect(profileRes.status).toBe(200);
});
테스트 접근 방식
블랙박스 테스트
- 내부 구현을 모르는 상태에서 테스트
- What에 집중 (어떤 결과가 나와야 하는가)
- 명세와 요구사항 기반 테스트
화이트박스 테스트
- 내부 구현을 아는 상태에서 테스트
- How까지 고려 (어떻게 동작하는가)
- 더 철저한 테스트 가능
테스트 대역의 활용
테스트 대역의 다섯 가지 유형과 특징:
- 더미(Dummy)
- 가장 단순한 형태
- 실제로 사용되지 않고 전달만 됨
- 스텁(Stub)
- 미리 준비된 응답 제공
- 상태 기반 테스트에 활용
- 스파이(Spy)
- 호출 정보를 기록
- 실제 객체처럼 동작하면서 정보 수집
- 목(Mock)
- 기대하는 동작 정의 및 검증
- 행위 검증에 중점
- 페이크(Fake)
- 실제 구현을 단순화한 대체제
- 실제와 유사하게 동작
각 대역의 선택은 테스트의 목적과 상황에 따라 결정해야 합니다.
실무 적용 사례
현재 팀의 테스트 전략
- 단위 테스트
- 서비스 레이어의 비즈니스 로직 검증
- DB 의존성 없는 순수 로직 테스트
- E2E 테스트
- API 엔드포인트 검증
- 실제 사용자 시나리오 기반 테스트
테스트 작성 프로세스
- API 설계 및 개발
- 시나리오 도출
- 테스트 케이스 작성
- AI 도구를 활용한 테스트 보완
728x90