Files
project-auth-server/docs/testing-coverage-policy.md

184 lines
9.0 KiB
Markdown

# 테스트 커버리지 정책
이 문서는 본 프로젝트의 **JaCoCo / PIT / jqwik** 테스트 정책과 임계치 근거, 그리고 baseline 측정값을 기록한다. 빌드 스크립트(`build.gradle`)는 이 문서의 결정을 강제한다.
> 변경 시 build.gradle 주석과 함께 갱신할 것.
---
## 1. 도구 역할 분담
| 도구 | 측정하는 것 | 한계 |
|---|---|---|
| **JaCoCo** | 라인/브랜치가 실제로 실행됐는가 | 단언이 약하면 가짜 안심 |
| **PIT** (mutation) | 단언이 실제로 결함을 잡아내는가 | 무겁다 — 변경 영역만 돌리는 게 현실적 |
| **jqwik** (property-based) | 정의한 불변(property)이 임의 입력 1만건에 대해서도 성립하는가 | 적용 가능 영역이 제한적(순수 함수, 변환, 검증) |
**원칙**: JaCoCo만으로는 부족. PIT가 진짜 척도. jqwik은 **타깃 한정** 도구.
---
## 2. Tier 분류와 임계치
본 정책은 **3단계** Tier로 운영한다. 설정/main/생성 코드는 Tier로 분류하지 않고 jacocoExclusions로 통째 제외한다(아래 "Tier 분류 외" 섹션).
### Tier 1 — Critical (95~100%)
*깨지면 보안/데이터/계약 손상. 정책 표현물 자체.*
| 패키지/클래스 | JaCoCo Line | JaCoCo Branch | PIT Mutation |
|---|---|---|---|
| `domain.user.exception.*` | 100% | 100% | 95% |
| `application.support.exception.*` | 100% | 100% | 95% |
| `application.support.logging.LogSanitizer` | 100% | 100% | 95% |
| `presentation.support.exception.ApiErrorHttpStatusMapper` | 100% | 100% | 100% |
| `presentation.support.exception.*Handler` | 95% | 90% | 85% |
| `bootstrap.config.web.ApiErrorController` | 100% | 95% | 90% |
| `bootstrap.config.web.SecurityResponseExceptionHandler` | 100% | 95% | 90% |
| `bootstrap.config.auth.security.SecurityExceptionHandler` | 100% | 95% | 90% |
### Tier 2 — Core (80~90%)
| 패키지/클래스 | JaCoCo Line | JaCoCo Branch | PIT Mutation |
|---|---|---|---|
| `application.*.usecase`, `application.*.service` | 85~90% | 80~85% | 70~75% |
| `presentation.*.controller`, `presentation.*.dto` | 85~90% | 80~85% | 70~75% |
| `infrastructure.persistence.*.adapter` | 85% | 80% | 70% |
| `presentation.support.response.*` | 90% | 85% | 75% |
### Tier 3 — Supporting (60~75%)
| 패키지/클래스 | JaCoCo Line | JaCoCo Branch | PIT Mutation |
|---|---|---|---|
| `infrastructure.security.bcrypt.*`, `infrastructure.security.jwt.*` | 80% | 75% | 65% |
| `infrastructure.security.vault.*` | 75% | 70% | 60% |
| `application.support.audit.*` | 80% | 75% | 65% |
| `presentation.support.logging` (TraceIdFilter 등) | 85% | 80% | 70% |
### Tier 분류 외 (커버리지 게이트 대상 아님)
다음 영역은 Tier로 묶지 않고 `jacocoExclusions`에서 통째로 제외한다 — 통합 테스트가 컨텍스트 로딩 과정에서 자연스럽게 거치므로 별도 단위 테스트가 무가치한 영역이다.
| 패키지/클래스 | 처리 | 비고 |
|---|---|---|
| `*Configuration` 클래스 (`@Configuration` 컨벤션) | jacocoExclusions에서 제외 | 통합 테스트가 컨텍스트 로딩으로 자연 커버. 빈 와이어링만 있고 분기 로직 없음. |
| `*Config` 클래스 (짧은 변형, 예: `OpenApiConfig`) | jacocoExclusions에서 제외 | 위와 동일 |
| `*Properties` 클래스 (`@ConfigurationProperties`) | jacocoExclusions에서 제외 | setter/getter 보일러플레이트 |
| `*Application` 클래스 (Spring Boot main) | jacocoExclusions에서 제외 | 의미 없음 |
| Flyway migrations | (코드 아님) | SQL은 별도 마이그레이션 테스트 |
> **중요**: 이전 정책의 `**/config/**` 광역 제외는 폐기됨. 그 패턴은 핵심 핸들러(ApiErrorController, SecurityResponseExceptionHandler, RequestBoundApiResultFactory 등 — 모두 `bootstrap/config/web/`, `bootstrap/config/auth/security/`에 위치)까지 같이 빼버려서 운영 빌드에서 진짜 측정값이 보이지 않았다. 클래스명 컨벤션 기반 제외로 좁혀, 동작 코드는 모두 측정 대상이 된다.
---
## 3. 제외 목록 (build.gradle `jacocoExclusions`)
```
- **/*Application.class # Spring Boot main
- **/*Configuration.class # @Configuration 빈 와이어링
- **/*Config.class # @Configuration 짧은 변형 (OpenApiConfig 등)
- **/*Properties.class # @ConfigurationProperties 보일러플레이트
- **/dto/**/*Request.class, *Response.class # boilerplate
- **/Q*.class # QueryDSL generated
- **/*$Builder.class # Lombok generated
- **/generated-sources/**
```
> **TODO**: `**/config/**` exclusion이 너무 광범위하다. 핸들러(ApiErrorController, SecurityResponseExceptionHandler)는 게이트에서 별도 강제하지만, 이상적으로는 exclusion 패턴을 좁혀 핸들러를 normal coverage report에 포함시키는 게 맞다. 다음 PR에서 정밀화.
---
## 4. 게이트 정책
### 즉시 적용 (현재)
- **PR 게이트**: `./gradlew coverageGate` — 명시적 호출 시에만 실행. CI에서 PR마다 자동 호출.
- **임계치**: build.gradle의 `jacocoTestCoverageVerification`에 모듈별로 표현됨.
- **PIT 게이트**: 활성화 완료 (2026-05-04). `./gradlew :application:pitest`, `:presentation:pitest`가 임계치 미달 시 빌드 실패한다.
- application: `mutationThreshold = 90`, `coverageThreshold = 90` (현재 측정 ≈ 94%)
- presentation: `mutationThreshold = 75`, `coverageThreshold = 90` (현재 측정 ≈ 77%)
### 6개월 후 (점진 상향)
- Tier 2 PIT 임계치 +5%p
- Tier 3 라인 +5%p
- presentation PIT mutationThreshold를 80~85%로 상향 (살아남은 변이 분석 후)
### 절대 금지
- 테스트 클래스에서 `@SuppressWarnings("...")`로 게이트 우회
- 단언 없는 테스트 (PIT가 잡지 못하는 경우 코드 리뷰에서 차단)
---
## 5. jqwik 적용 클래스 목록 (의무)
다음 클래스는 jqwik 속성 기반 테스트가 **반드시** 존재해야 한다. 신규 클래스 추가 시 코드 리뷰에서 확인.
| 클래스 | 테스트 파일 | 검증 속성 |
|---|---|---|
| `LogSanitizer` | `LogSanitizerPropertyTest` | 길이 한도, 제어문자 제거, IPv4 마스킹, 이메일 마스킹, hash prefix |
| `ApiErrorHttpStatusMapper` | `ApiErrorHttpStatusMapperPropertyTest` | 모든 enum이 4xx/5xx 매핑, 결정론, 누락 검출 |
| `ValidationExceptionHandler.fieldFieldToJsonPointer` | `JsonPointerConversionPropertyTest` | RFC 6901 형식, 이스케이프(`~`, `/`), dotted/indexed/nested 변환 |
| `RequestBoundApiResultFactory` | `RequestBoundApiResultFactoryPropertyTest` | traceId 폴백, sentinel 동작, 일관된 envelope |
권장 (다음 PR):
- 모든 토큰 파서/검증 로직
- 모든 변환(transform) 함수
- 모든 정규화(normalize) 함수
---
## 6. Baseline 측정 (2026-05-03)
### JaCoCo (모듈별 단위 + 통합 테스트 전체 실행 후)
| 모듈 | Line | Branch |
|---|---|---|
| `application` | 70.9% | 78.9% |
| `presentation` | 24.8% | 42.3% |
| `infrastructure` | 81.5% | — |
| `bootstrap` | 0.0% | — *(현 시점 `**/config/**` exclusion으로 핵심 클래스가 모두 제외됨; 다음 PR에서 정밀화)* |
> **해석**: presentation 모듈 단위 테스트가 핸들러 본문을 거의 안 거치는 이유는 통합 테스트가 bootstrap 모듈에 있기 때문. 멀티모듈 aggregate 리포트가 다음 단계 작업.
### PIT (현재 측정 가능 영역)
| 모듈 | Targets | Mutations | Killed | Test Strength |
|---|---|---|---|---|
| `application` | `support.exception.*`, `support.logging.*` | 64 | 46 (72%) | 82% |
| `presentation` | `support.exception.*`, `support.response.*` | 75 | 24 (32%) | **92%** *(coverage 자체가 24%)* |
> **해석**: presentation의 32% mutation kill은 *coverage가 낮기 때문*이지, 단언이 약해서가 아니다. coverage된 코드 안에서의 test strength는 92%로 매우 높다 → 단언이 의미가 있다는 증거. 통합 테스트가 PIT에 잡히도록 하는 것이 다음 단계.
---
## 7. 명령어 치트시트
```bash
# 전체 테스트 + JaCoCo 리포트 (게이트 없음 — 일반 빌드)
./gradlew test jacocoTestReport
# 게이트 검증 (CI/PR에서)
./gradlew coverageGate
# PIT baseline 측정 (수동/주간 CI)
./gradlew mutationBaseline
# 특정 모듈만
./gradlew :application:pitest
./gradlew :presentation:pitest
# JaCoCo 리포트 위치
{module}/build/reports/jacoco/test/html/index.html
# PIT 리포트 위치
{module}/build/reports/pitest/index.html
```
---
## 8. 다음 작업
1. **`**/config/**` exclusion 정밀화** — 핵심 핸들러 4개를 normal coverage report에 포함.
2. **멀티모듈 aggregate JaCoCo 리포트** — 통합 테스트가 다른 모듈 코드를 커버하는 정도를 정확히 측정.
3. **CI에서 `coverageGate` 자동 실행** — PR 단위 게이트 활성화.
4. **PIT mutation 점수 게이트 활성화 (6개월 후)** — Tier 1: 85%+, Tier 2: 70%+.
5. **MessageSource 도입 후** ClientFacingErrorCode i18n 전환 — 별도 메모리에 등록됨.