단위 테스트 설계하기
1. 왜 테스트를 작성하는가?
테스트를 먼저 생각하면 더 나은 API 설계가 나온다는 것이 TDD의 핵심 철학이다. 테스트 코드를 작성하면서 얻는 이점은 다음과 같다.
- 리팩토링 안전망: 코드 변경이 수반되는 경우에도 기능이 동작함을 보장할 수 있다
- 빠른 검증: 컴파일/실행 없이 빠르게 검증할 수 있다
- 설계 개선: 테스트하기 어려운 코드는 대체로 설계가 좋지 않다는 신호다
2. 어떤 코드에 테스트가 필요한가?
내가 만든 로직은 테스트하고, 남이 만든 건 테스트하지 않는다!
테스트하지 않아도 되는 코드
일회성 스크립트
테스트 범위는 프로덕션으로 한정한다. 프로덕션에 들어가지 않는 일회성 스크립트는 테스트 대상이 아니다.
외부 라이브러리
외부 라이브러리는 의존성에 해당한다. 테스트는 ‘내가 작성한 코드’에 한정한다.
프레임워크 코드
프레임워크가 이미 테스트한 기능은 다시 테스트하지 않는다.
@router.get("/health")
def get_health():
return {"status": "healthy"}
위 코드에서 라우터가 진짜 라우팅을 하는지 검증하는 test_router_exists를 만들 필요는 없다. SQLAlchemy로 정의된 모델이 진짜 존재하는지를 getattr()로 확인할 필요도 없다.
그러면 라우터는 어떻게 테스트할까?
- 서비스 계층에서 테스트: 비즈니스 로직을 담당하는 서비스 계층을 테스트한다
- 통합 테스트: 전체 흐름을 통합 테스트에서 검증한다
단, 모델에 비즈니스 로직이 있을 때는 테스트한다. 예를 들어 모델을 만들 때 updated_at을 자동으로 업데이트하는 로직이 있다면, 그 로직은 테스트 대상이다.
트리비얼 코드
너무 단순해서 버그가 생길 일이 없는 코드는 테스트하지 않는다.
- Pydantic 모델로 만들어진 데이터 클래스 등 자체 검증이 내장된 클래스
- 단순한 getter/setter
다만, 같은 getter/setter 패턴이어도 비즈니스 로직에 따라 값을 설정하는 setter는 테스트해야 한다.
3. 단위 테스트가 다른 테스트와 다른 점
테스트의 종류에는 단위 테스트, 통합 테스트, E2E 테스트가 있다.
단위 테스트 (Unit Test)
- 함수, 메서드, 클래스 단위로 테스트
- 외부 의존성 없음 (Mock 사용)
통합 테스트 (Integration Test)
- 여러 모듈 간의 상호작용 테스트
- 실제 의존성 사용
- 실제 데이터베이스와 연결해서 하는 테스트
- 실제 외부 API를 호출하는 테스트
E2E 테스트 (End-to-End Test)
- 전체 시스템을 사용자 관점에서 테스트
- 실제 환경과 동일하게 구성
- QA와 비슷한 역할?
- 중요한 기능을 이중 검증하기 위해 수행
4. 좋은 테스트의 조건
빠르게 실행되어야 한다
느린 테스트는 개발 흐름을 방해한다.
독립적이어야 한다
다른 테스트에 영향을 주거나 받지 않아야 한다.
반복 가능해야 한다
같은 테스트를 여러 번 실행해도 같은 결과가 나와야 한다. 시간에 따라 실패하거나 간헐적으로 실패하는 테스트를 작성하지 않는다.
자동으로 판단해야 한다
테스트 결과를 사람이 판단하지 않아야 한다. assert로 명확하게 판단한다.
Red-Green-Refactor 패턴을 따른다
1. RED: 먼저 실패하는 테스트를 작성한다
2. GREEN: 테스트를 통과하는 최소한의 코드를 작성한다
3. REFACTOR: 코드를 리팩토링한다
공개 API에 집중한다
테스트는 공개 API에 집중하고 내부 구현부는 테스트하지 않는다.
예를 들어 주문을 처리하는 엔드포인트가 있고, 내부적으로 품목 조회 → 주문 저장 과정이 있다면, “주문이 처리되었는지”를 테스트하면 되지 “품목 조회”와 “주문 저장”을 개별적으로 테스트하지 않는다.
엣지케이스를 테스트한다
숫자
- 0, 1, -1
- 최댓값, 최솟값
- 음수, 양수
- 정수, 소수
문자열
- 빈 문자열
"" - 공백
" " - 매우 긴 문자열
- 특수문자, 유니코드
None/null
컬렉션
- 빈 리스트
[] - 단일 원소
[1] - 중복 원소
None
날짜/시간
- 윤년, 평년
- 월말 (28, 29, 30, 31일)
- 시간대 경계
- 과거, 미래, 현재
테스트 커버리지
커버리지를 높이는 것은 중요하지만, 그보다 더 중요한 것은 테스트 케이스를 잘 마련해두는 것이다.
# pytest-cov 설치
pip install pytest-cov
# 커버리지 측정
pytest --cov=src --cov-report=html
# HTML 리포트 확인
open htmlcov/index.html
5. 어떤 코드를 먼저 테스트해야 할까?
모든 코드가 같은 중요도를 가지지 않으므로 다음 기준에 따라 우선순위를 정한다.
| 우선순위 | 대상 | 목표 커버리지 |
|---|---|---|
| 1순위 | 비즈니스 핵심 로직 | 80~90% |
| 2순위 | 자주 변경되는 코드 | 70~80% |
| 3순위 | 안정적인 유틸리티 | 50~60% |
| 4순위 | 프레임워크 래퍼 | 테스트 불필요 |
비즈니스 핵심 로직이 가장 중요하다. 돈이 오가는 로직, 사용자 데이터를 다루는 로직 등이 여기에 해당한다.
자주 변경되는 코드는 회귀 버그 발생 가능성이 높으므로 테스트로 보호해야 한다.
안정적인 유틸리티는 한 번 작성하면 잘 변경되지 않으므로 상대적으로 낮은 우선순위를 가진다.
프레임워크 래퍼는 프레임워크가 이미 테스트하고 있으므로 별도 테스트가 불필요하다.