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

9.0 KiB

테스트 커버리지 정책

이 문서는 본 프로젝트의 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. 명령어 치트시트

# 전체 테스트 + 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 전환 — 별도 메모리에 등록됨.