🔧 DevOps

Unit Test

유닛 테스트

개별 함수/모듈 테스트. 가장 작은 테스트 단위. Jest, pytest.

상세 설명

유닛 테스트(Unit Test)는 소프트웨어의 가장 작은 단위인 개별 함수, 메서드, 클래스를 독립적으로 검증하는 테스트입니다. 1970년대 Kent Beck의 SUnit(Smalltalk)에서 시작되어, 현재는 모든 언어에 테스트 프레임워크가 존재합니다.

좋은 유닛 테스트의 특성은 FIRST 원칙으로 정리됩니다. Fast(빠른 실행), Independent(테스트 간 독립), Repeatable(동일 결과 반복), Self-Validating(자동 판정), Timely(적시 작성). 외부 의존성(DB, API)은 Mock으로 대체해 테스트 격리를 보장합니다.

테스트 구조는 AAA 패턴을 따릅니다. Arrange(준비: 테스트 데이터와 환경 설정), Act(실행: 테스트 대상 함수 호출), Assert(검증: 결과가 기대값과 일치하는지 확인). 각 테스트는 하나의 동작만 검증해야 실패 원인 파악이 쉽습니다.

실무에서는 Jest(JavaScript), pytest(Python), JUnit(Java), Go test 등을 사용합니다. 테스트 커버리지 80% 이상을 목표로 하되, 핵심 비즈니스 로직과 복잡한 조건 분기에 집중합니다. CI에서 PR마다 자동 실행되어 회귀 버그를 방지합니다.

코드 예제

// Jest를 사용한 유닛 테스트 예제

// src/services/priceCalculator.js
export function calculateDiscount(price, userType, couponCode) {
  if (price <= 0) throw new Error('Price must be positive');

  let discount = 0;

  // 회원 등급별 할인
  if (userType === 'premium') discount += 0.1;
  else if (userType === 'vip') discount += 0.2;

  // 쿠폰 할인
  if (couponCode === 'SAVE10') discount += 0.1;
  else if (couponCode === 'SAVE20') discount += 0.2;

  // 최대 할인율 제한
  discount = Math.min(discount, 0.3);

  return Math.round(price * (1 - discount));
}

// src/services/priceCalculator.test.js
import { calculateDiscount } from './priceCalculator';

describe('calculateDiscount', () => {
  // 기본 동작 테스트
  describe('기본 할인 계산', () => {
    it('할인 없이 원래 가격을 반환한다', () => {
      // Arrange
      const price = 10000;

      // Act
      const result = calculateDiscount(price, 'regular', null);

      // Assert
      expect(result).toBe(10000);
    });
  });

  // 회원 등급별 할인
  describe('회원 등급 할인', () => {
    it('premium 회원은 10% 할인', () => {
      expect(calculateDiscount(10000, 'premium', null)).toBe(9000);
    });

    it('vip 회원은 20% 할인', () => {
      expect(calculateDiscount(10000, 'vip', null)).toBe(8000);
    });
  });

  // 쿠폰 할인
  describe('쿠폰 할인', () => {
    it.each([
      ['SAVE10', 9000],
      ['SAVE20', 8000],
      ['INVALID', 10000],
    ])('쿠폰 %s 적용 시 %d원', (coupon, expected) => {
      expect(calculateDiscount(10000, 'regular', coupon)).toBe(expected);
    });
  });

  // 복합 할인 및 제한
  describe('복합 할인', () => {
    it('VIP + SAVE20 = 최대 30% 할인 적용', () => {
      // VIP 20% + 쿠폰 20% = 40% 이지만 최대 30%로 제한
      expect(calculateDiscount(10000, 'vip', 'SAVE20')).toBe(7000);
    });
  });

  // 엣지 케이스
  describe('엣지 케이스', () => {
    it('가격이 0이하면 에러', () => {
      expect(() => calculateDiscount(0, 'regular', null))
        .toThrow('Price must be positive');
    });

    it('가격이 음수면 에러', () => {
      expect(() => calculateDiscount(-100, 'regular', null))
        .toThrow('Price must be positive');
    });
  });
});

// 외부 의존성 Mocking 예제
// src/services/userService.test.js
import { getUser } from './userService';
import { db } from '../db';

// db 모듈 전체 Mock
jest.mock('../db');

describe('getUser', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('존재하는 사용자를 반환한다', async () => {
    // Mock 설정
    db.users.findById.mockResolvedValue({
      id: 1,
      name: 'John'
    });

    const user = await getUser(1);

    expect(user.name).toBe('John');
    expect(db.users.findById).toHaveBeenCalledWith(1);
  });

  it('존재하지 않는 사용자는 null 반환', async () => {
    db.users.findById.mockResolvedValue(null);

    const user = await getUser(999);

    expect(user).toBeNull();
  });
});

실무에서 이렇게 말해요

시니어: "결제 로직 변경했는데 기존 유닛 테스트가 다 깨졌어요. 테스트가 구현 세부사항에 너무 의존하고 있네요."

주니어: "내부 메서드 호출 순서까지 검증해서 그런 것 같아요. 입력-출력만 검증하도록 리팩토링할까요?"

시니어: "네, 블랙박스 테스트처럼 작성하면 리팩토링해도 안 깨져요."

면접관: "테스트 작성 시 어떤 점을 중요하게 생각하시나요?"

지원자: "테스트가 구현이 아닌 동작을 검증해야 합니다. 내부 구현이 바뀌어도 테스트가 깨지면 안 됩니다. 또한 테스트 이름만 보고도 무엇을 검증하는지 알 수 있어야 하고, 실패 시 원인을 바로 파악할 수 있도록 하나의 테스트에서 하나의 동작만 검증합니다."

리뷰어: "이 함수에 조건 분기가 4개인데 테스트가 1개뿐이에요. 엣지 케이스 테스트 추가해주세요."

개발자: "null 입력, 빈 배열, 경계값 케이스 추가하겠습니다. it.each로 파라미터화된 테스트로 만들면 깔끔하겠네요."

주의사항

더 배우기