테스트 원칙
언어별 참조
언어별 테스트 패턴에 대해서는 다음 문서도 함께 읽으십시오:
- TypeScript (Vitest/RTL/Playwright): references/typescript.md
핵심 테스트 철학
- 테스트는 일급 코드다: 프로덕션 코드와 동등한 수준으로 테스트 품질을 유지하십시오.
- 빠른 피드백: 테스트는 빠르게 실행되고 즉각적인 피드백을 제공해야 합니다.
- 신뢰성: 테스트는 결정론적이고 재현 가능해야 합니다.
- 독립성: 각 테스트는 격리된 환경에서 실행되어야 합니다.
TDD 프로세스 [모든 코드 변경에 필수]
모든 코드 변경 시 이 프로세스를 실행하십시오:
RED 단계
단계 1: 예상 동작을 정의하는 테스트를 작성하십시오. 단계 2: 테스트를 실행하십시오. 단계 3: 테스트가 실패하는지 확인하십시오 (통과하면 테스트가 잘못된 것입니다).
GREEN 단계
단계 1: 테스트를 통과시키는 최소한의 코드를 작성하십시오. 단계 2: 테스트를 실행하십시오. 단계 3: 테스트가 통과하는지 확인하십시오.
REFACTOR 단계
단계 1: 코드 품질을 개선하십시오 (중복 제거, 이름 개선). 단계 2: 테스트를 실행하십시오. 단계 3: 테스트가 여전히 통과하는지 확인하십시오.
VERIFY 단계 [필수 - 0 오류 요구]
단계 1: 언어/프로젝트에 대한 모든 품질 검사 명령을 실행하십시오. 단계 2: 모든 명령이 0 오류로 통과할 때까지 오류를 수정하십시오. 단계 3: 회귀가 없음을 확인하십시오.
강제 규칙: 품질 검사에 실패하면 다음 단계로 진행할 수 없습니다.
TDD 예외 (TDD 불필요)
- 순수 설정 파일
- 문서 전용
- 긴급 수정 (단, 수정 직후 즉시 테스트를 추가하십시오)
- 탐색적 스파이크 (병합 전에 테스트로 재작성하거나 폐기하십시오)
- 빌드/배포 스크립트 (비즈니스 로직이 포함된 경우 제외)
품질 요구 사항 [필수]
커버리지
- 커버리지를 목표가 아닌 테스트되지 않은 영역을 찾기 위한 진단 신호로 취급하십시오. 목표는 사소한 테스트로 조작될 수 있습니다.
- 회귀가 중요할 동작, 비즈니스 로직, 중요 경로에 테스트를 집중하십시오.
- 커버리지 숫자보다 의미 있는 단언(assertion)에 우선순위를 두십시오. 필요한 임계값은 프로젝트의 CI, 작업 파일, 작업 계획 또는 설계 문서에서 가져옵니다.
테스트 특성
모든 테스트는 다음을 반드시 충족해야 합니다:
- 독립성: 테스트 간 의존성이 없어야 합니다.
- 재현 가능성: 동일한 입력은 항상 동일한 출력을 생성해야 합니다.
- 속도: 프로젝트에서 허용하는 피드백 시간 내에 전체 테스트 스위트를 완료하고, 로컬 반복 또는 CI를 현저히 느리게 하는 스위트를 표시해야 합니다.
- 자가 검증: 수동 확인 없이 명확히 통과/실패를 판단할 수 있어야 합니다.
- 적시성: 테스트하는 코드와 가깝게 작성되어야 합니다.
강제 규칙: 위 특성 중 하나라도 실패하는 테스트는 즉시 수정되어야 합니다.
테스트 유형
단위 테스트
목적: 개별 컴포넌트를 격리하여 테스트합니다.
특성:
- 단일 함수, 메서드 또는 클래스를 테스트합니다.
- 빠른 실행 (밀리초)
- 외부 의존성 없음
- 외부 서비스는 모의(mock) 처리
- 테스트 스위트의 대부분을 차지
통합 테스트
목적: 컴포넌트 간 상호 작용을 테스트합니다.
특성:
- 여러 컴포넌트를 함께 테스트합니다.
- 데이터베이스, 파일 시스템 또는 API를 포함할 수 있습니다.
- 단위 테스트보다 느립니다.
- 모듈 간의 계약을 검증합니다.
- 테스트 스위트에서 작은 부분을 차지
종단간(E2E) 테스트
목적: 사용자 관점에서 완전한 워크플로우를 테스트합니다.
특성:
- 전체 애플리케이션 스택을 테스트합니다.
- 실제 사용자 상호 작용을 시뮬레이션합니다.
- 가장 느린 테스트 유형
- 가장 적은 수
- 가장 높은 신뢰도
테스트 피라미드
다음 테스트 피라미드 구조를 따르십시오:
/\ <- 소수의 E2E 테스트 (높은 신뢰도, 느림)
/ \
/ \ <- 일부 통합 테스트 (중간 신뢰도, 중간 속도)
/ \
/________\ <- 많은 단위 테스트 (빠름, 기초)
테스트 설계 원칙
AAA 패턴 (Arrange-Act-Assert)
모든 테스트를 세 가지 명확한 단계로 구성하십시오:
// Arrange: 테스트 데이터와 조건 설정
user = createTestUser()
validator = createValidator()
// Act: 테스트 대상 코드 실행
result = validator.validate(user)
// Assert: 예상 결과 확인
assert(result.isValid == true)
개념당 하나의 단언
- 테스트 케이스당 하나의 동작을 테스트하십시오.
- 단일 개념을 테스트하는 경우 여러 단언은 허용됩니다.
- 관련 없는 단언은 별도의 테스트로 분리하십시오. 하나의 테스트는 반드시 하나의 동작을 검증해야 합니다.
설명적인 테스트 이름
테스트 이름은 다음을 명확히 설명해야 합니다:
- 무엇을 테스트하는지
- 어떤 조건에서
- 예상 결과는 무엇인지
권장 형식: "should [예상 동작] when [조건]"
테스트 독립성
격리 요구 사항
- 각 테스트는 자체 테스트 데이터를 생성합니다.
- 실행 순서에 대한 의존성이 없습니다.
- 자체 상태를 정리합니다.
- 격리되어 실행될 때 통과해야 합니다.
설정 및 정리
- 설정 훅을 사용하여 테스트 환경을 준비하십시오.
- 정리 훅을 사용하여 리소스를 정리하십시오.
- 설정은 테스트 대상 동작에 필요한 데이터, 의존성 및 픽스처로 범위를 한정하십시오.
- 테스트가 실패하더라도 정리가 실행되도록 보장하십시오.
모의(mock) 및 테스트 대역
모의를 사용해야 하는 경우
- 외부 의존성 모의: API, 데이터베이스, 파일 시스템
- 느린 작업 모의: 네트워크 호출, 대규모 계산
- 예측 불가능한 동작 모의: 난수, 현재 시간
- 사용 불가능한 서비스 모의: 타사 서비스
모의 원칙 [필수]
- 경계에서 모의하고 내부는 모의하지 마십시오. 내부 유틸리티에는 실제 구현을 사용하십시오.
- 각 모의는 테스트가 제어하거나 관찰해야 하는 동작으로 제한하십시오.
- 관련이 있을 때 모의 기대치를 검증하십시오.
- 제어할 수 없는 외부 라이브러리/프레임워크에는 어댑터를 사용하십시오.
테스트 대역 유형
- Stub: 미리 결정된 값을 반환합니다.
- Mock: 올바르게 호출되었는지 검증합니다.
- Spy: 호출에 대한 정보를 기록합니다.
- Fake: 단순화된 작동 구현입니다.
- Dummy: 전달되지만 사용되지 않습니다.
데이터 계층 테스트
데이터 접근에 대한 모의 한계
모의는 호출 패턴을 검증하지만 스키마 정확성, 쿼리 정확성 또는 저장소 제약 조건을 검증하지 않습니다. 모의가 놓칠 수 있는 문제의 예:
- 스키마 드리프트
- 컬럼 또는 필드 불일치
- 잘못된 조인, 필터 또는 집계
- 마이그레이션 비호환성
실제 데이터 계층 검증이 가치를 더하는 경우
다음을 테스트할 때 실제 또는 프로덕션과 유사한 데이터 접근 검증을 사용하십시오:
- 저장소 또는 DAO 구현
- ORM 매핑
- 쿼리 빌더 또는 원시 SQL
- 제약 조건이나 스키마 형태에 의존하는 지속성 동작
환경 옵션
프로젝트 환경에 가장 실용적인 옵션을 선택하십시오:
- 컨테이너화된 데이터베이스
- 전용 테스트 데이터베이스
- 문서화된 제한 사항이 있는 인메모리 데이터베이스
- 어댑터 기반 로컬 테스트 하네스
설계 정렬
설계 문서에 Test Boundaries가 포함된 경우 이를 기준으로 삼아 어떤 의존성을 실제로 유지하고 어떤 경계를 격리할지 결정하십시오.
테스트 품질 관행 [필수]
테스트를 활성 상태로 유지
- 실패하는 테스트 수정 또는 삭제: 실패를 즉시 해결하십시오.
- 주석 처리된 테스트 제거: 수정하거나 완전히 삭제하십시오.
- 테스트를 계속 실행: 테스트가 깨지면 가치가 빠르게 떨어집니다.
- 테스트 스위트 유지 관리: 필요에 따라 테스트를 리팩터링하십시오.
테스트 코드 품질
- 프로덕션 코드와 동일한 기준을 적용하십시오.
- 설명적인 변수 이름을 사용하십시오.
- 중복을 줄이기 위해 테스트 헬퍼를 추출하십시오.
- 테스트를 읽기 쉽고 유지 관리 가능하게 유지하십시오.
테스트 헬퍼 및 유틸리티
- 재사용 가능한 테스트 데이터 빌더를 만드십시오.
- 공통 설정을 헬퍼 함수로 추출하십시오.
- 복잡한 시나리오를 위한 테스트 유틸리티를 구축하십시오.
- 테스트 파일 간에 헬퍼를 적절히 공유하십시오.
테스트할 대상
동작에 집중
구현이 아닌 관찰 가능한 동작을 테스트하십시오:
- 좋음: 함수가 예상 출력을 반환하는지 테스트
- 좋음: 올바른 API 엔드포인트가 호출되는지 테스트
- 나쁨: 내부 변수가 설정되었는지 테스트
- 나쁨: 비공개 메서드 호출 순서 테스트
경계 사례 테스트
항상 다음을 테스트하십시오:
- 경계 조건: 최소/최대값, 빈 컬렉션
- 오류 사례: 잘못된 입력, null 값, 누락된 데이터
- 경계 사례: 특수 문자, 극단적인 값
- 해피 패스: 정상적이고 예상되는 사용법
테스트 품질 기준 [필수]
- 리터럴 기대치: 단언문에 하드코딩된 리터럴 값을 사용하십시오. 예상 값 ≠ 모의 반환 값 (구현은 데이터를 처리합니다).
- 결과 검증: 호출 순서가 아닌 반환 값과 상태를 단언하십시오.
- 의미 있는 단언: 모든 테스트는 최소 하나의 단언을 가져야 합니다. 단언문이 없는 테스트는 가치가 없습니다.
- 외부 I/O만 모의: DB/API/파일시스템을 모의하고, 내부 유틸리티는 실제를 사용하십시오.
- 경계 커버리지: 해피 패스와 함께 빈/영/최대/오류 사례를 포함하십시오.
강제 규칙: 기준을 위반하는 테스트는 반드시 재작성되어야 합니다.
검증 요구 사항 [VERIFY 단계에 필수]
커밋 전 체크리스트
☐ 모든 테스트 통과 ☐ 건너뛰거나 주석 처리된 테스트 없음 ☐ 테스트에 디버그 코드가 남아 있지 않음 ☐ 프로젝트, 작업 파일, 작업 계획 또는 설계 문서에서 정의된 경우 커버리지 임계값 통과 ☐ 테스트가 합리적인 시간 내에 실행됨
무관용 정책
- 실패 테스트 0개: 즉시 수정
- 건너뛴 테스트 0개: 삭제 또는 수정
- 불안정한 테스트 0개: 결정론적으로 만들기
- 느린 테스트 0개: 최적화 또는 분할
강제 규칙: 품질 검사가 하나라도 실패하면 작업 완료를 진행할 수 없습니다.
테스트 조직
파일 구조
- 프로덕션 구조 미러링: 테스트는 코드 구성을 따릅니다.
- 명확한 명명 규칙: 프로젝트의 테스트 파일 패턴을 따르십시오.
- 논리적 그룹화: 관련 테스트를 함께 그룹화하십시오.
- 별도의 테스트 유형: 단위, 통합, E2E 테스트를 별도의 디렉토리에 배치하십시오.
테스트 스위트 조직
tests/
├── unit/ # 빠르고 격리된 단위 테스트
├── integration/ # 통합 테스트
├── e2e/ # 종단간 테스트
├── fixtures/ # 테스트 데이터 및 픽스처
└── helpers/ # 공유 테스트 유틸리티
성능 고려 사항
테스트 속도
- 단위 테스트: 각각 < 100ms
- 통합 테스트: 각각 < 1s
- 전체 스위트: 자주 실행되어야 함 (< 10분)
일반적인 안티 패턴
다음 패턴을 즉시 발견하고 제거하십시오:
- 아무것도 테스트하지 않는 테스트 (항상 통과)
- 실행 순서에 의존하는 테스트
- 외부 상태에 의존하는 테스트
- 복잡한 로직이 있는 테스트 (자체 테스트가 필요한 테스트)
- 관찰 가능한 동작 대신 구현 세부 사항을 테스트하는 것
- 과도한 모의 (경계만 모의하고 내부는 실제를 사용)
- 테스트 코드 중복
불안정한 테스트
간헐적으로 실패하는 테스트를 제거하십시오:
- 타이밍 종속성 제거
- 난수 대신 결정론적 데이터 사용
- 적절한 정리 보장
- 경쟁 조건 수정
- 모든 테스트를 결정론적으로 만들기
회귀 테스트
- 모든 버그 수정에 대한 테스트를 추가하십시오.
- 포괄적인 테스트 스위트를 유지 관리하십시오.
- 정기적으로 전체 스위트를 실행하십시오.
- 테스트는 해당 테스트가 다루는 동작이 더 이상 존재하지 않거나 동일한 동작이 올바른 수준의 더 강력한 테스트로 대체된 경우에만 삭제하십시오.
레거시 코드
- 리팩터링 전에 특성화 테스트를 추가하십시오.
- 먼저 기존 동작을 테스트하십시오.
- 점진적으로 커버리지를 개선하십시오.
- 자신 있게 리팩터링하십시오.