✍🏻/독서록

테스트 코드 현실 적용기: Unit Testing 책을 통한 인사이트와 실무 경험

foodev 2025. 1. 29. 22:19
728x90

Unit Testing 책 리뷰와 실무 적용기

들어가며

"Unit Testing: Principles, Practices, and Patterns"(Vladimir Khorikov 저) 책을 읽고 실무에 적용하면서 느낀 점을 정리해보았습니다. 테스트 코드를 작성하는 여러 방법론과 실제 현장에서의 적용 과정에서 배운 점들을 공유하고자 합니다.
(실은 읽다가 어려워서 누락된 내용도 꽤 많아요 ㅎㅎ이해하고 넘어가주세요)

책의 핵심 내용

테스트의 기본 원칙

  1. 테스트는 비즈니스 시나리오를 반영해야 함
  2. 테스트 가독성이 중요
  3. 테스트가 구현이 아닌 동작을 검증해야 함

단위 테스트의 정의

  1. 작은 코드조각을 검증하고
  2. 빠르게 수행하고
  3. 격리된 방식으로 처리하는 자동화된 테스트

고전파 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();
});

 

 

두 학파의 주요 차이점

  1. 테스트 주도 개발을 통한 시스템 설계 방식
  2. 과도한 명세 문제

테스트 주도 개발(TDD)

TDD의 핵심 원칙

  1. 실패하는 테스트를 먼저 작성
  2. 코드가 완벽하지 않아도 됨
  3. 읽기 쉽고 유지보수가 용이하도록 작성

실무 적용 경험

프로젝트 진행 과정

  1. 기획 완료 후 프론트엔드/백엔드 동시 개발 시작
  2. 데이터 모델 설계
  3. Swagger를 통한 API 문서화 및 소통
  4. 개발 완료 후 API 테스트 및 QA

실무에서 겪은 어려움과 해결책

  • Swagger 문서 작성이 TDD로 인해 지연되는 문제 발생
  • 해결책: Swagger용 mock 객체 먼저 생성 후 TDD 진행
  • 현재는 API 개발 후 AI 도움을 받아 테스트 케이스 작성

테스트 유형별 특징

통합 테스트와 E2E 테스트

  • 통합 테스트: 여러 컴포넌트간 상호작용 검증
  • E2E 테스트: 전체 시스템 흐름 테스트

테스트 대역(Test Double)의 이해

  1. 더미(Dummy): 단순 전달용 객체
  2. 스텁(Stub): 미리 준비된 응답 제공
  3. 스파이(Spy): 메서드 호출 기록
  4. 목(Mock): 예상 동작 검증
  5. 페이크(Fake): 간소화된 실제 구현체

블랙박스/화이트박스 테스트

  • 블랙박스: 내부 구조를 모르는 상태에서 테스트
  • 화이트박스: 내부 구조를 아는 상태에서 테스트

테스트 피라미드

테스트 코드의 이상적인 구조

단위 테스트 >> 통합 테스트 >> E2E 테스트 순으로 작성 권장

피라미드 구조를 권장하는 이유

  1. E2E 테스트의 한계
    • 높은 유지보수 비용
    • 긴 실행 시간
    • 불안정성 (깨지기 쉬움)
  2. 단위 테스트의 장점
    • 빠른 실행 속도
    • 안정적인 결과
    • 문제 발생 지점 특정 용이

테스트 유형별 코드 예시

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까지 고려 (어떻게 동작하는가)
  • 더 철저한 테스트 가능

테스트 대역의 활용

테스트 대역의 다섯 가지 유형과 특징:

  1. 더미(Dummy)
    • 가장 단순한 형태
    • 실제로 사용되지 않고 전달만 됨
  2. 스텁(Stub)
    • 미리 준비된 응답 제공
    • 상태 기반 테스트에 활용
  3. 스파이(Spy)
    • 호출 정보를 기록
    • 실제 객체처럼 동작하면서 정보 수집
  4. 목(Mock)
    • 기대하는 동작 정의 및 검증
    • 행위 검증에 중점
  5. 페이크(Fake)
    • 실제 구현을 단순화한 대체제
    • 실제와 유사하게 동작

각 대역의 선택은 테스트의 목적과 상황에 따라 결정해야 합니다.

실무 적용 사례

현재 팀의 테스트 전략

  1. 단위 테스트
    • 서비스 레이어의 비즈니스 로직 검증
    • DB 의존성 없는 순수 로직 테스트
  2. E2E 테스트
    • API 엔드포인트 검증
    • 실제 사용자 시나리오 기반 테스트

테스트 작성 프로세스

  1. API 설계 및 개발
  2. 시나리오 도출
  3. 테스트 케이스 작성
  4. AI 도구를 활용한 테스트 보완
728x90