클라우드 Mac에서 xccov로 iOS 코드 커버리지 회귀 게이트 구축

클라우드 Mac에서 xccov로 iOS 코드 커버리지 회귀 게이트 구축

단위 테스트 묶음을 클라우드 Mac으로 이전한 뒤 팀이 가장 흔히 저지르는 실수는 ‘테스트 통과 여부’만 확인하는 것입니다. 어떤 커밋에서 핵심 분기의 단언문이 삭제되어도 테스트는 모두 통과할 수 있지만, 커버리지는 조용히 하락합니다. 더 안정적인 방법은 빌드마다 독립된 xcresult를 생성하고 Xcode에 내장된 xccov로 데이터를 추출한 다음, 프로젝트 전체 커버리지와 이번 변경에 포함된 파일을 함께 검사하는 것입니다.

커버리지 입력 조건 고정

커버리지는 테스트 진입점이 일정할 때만 비교할 가치가 있습니다. 먼저 Xcode 버전, scheme, 테스트 계획, 시뮬레이터 모델과 OS 버전을 고정하고, 실행기마다 destination을 임의로 선택하게 두지 마세요. 테스트 명령에서도 커버리지를 명시적으로 활성화하고 실행할 때마다 새로운 결과 디렉터리를 만들어야 합니다.

set -euo pipefail

RESULT_DIR="$PWD/Artifacts/Coverage"
RESULT_BUNDLE="$RESULT_DIR/TestResults.xcresult"

rm -rf "$RESULT_BUNDLE"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace ExampleApp.xcworkspace \
  -scheme ExampleApp-CI \
  -testPlan UnitTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -enableCodeCoverage YES \
  -resultBundlePath "$RESULT_BUNDLE"

resultBundlePath가 가리키는 경로는 실행 전에 존재하지 않아야 합니다. 이전 결과가 남아 있으면 새 결과를 쓰지 못할 수 있습니다. CI 스크립트에는 xcodebuild -version, 현재 커밋 식별자와 전체 destination도 기록해야 합니다. 그래야 도구 체인 변경을 코드 회귀로 잘못 판단하지 않습니다.

커버리지 게이트는 동일한 테스트 조건에서 발생한 변화를 측정하는 수단이지, 서로 다른 시뮬레이터나 테스트 계획을 가로질러 순위를 매기는 지표가 아닙니다.

xcresult에서 감사 가능한 데이터 추출

테스트가 끝나면 터미널의 서식화된 텍스트를 파싱하지 말고 xccov로 JSON을 내보내세요. JSON은 파일 경로, 대상 이름과 라인 커버리지를 보존하기에 더 적합하며, 표시 너비가 바뀌어도 열이 어긋나지 않습니다.

xcrun xccov view \
  --report \
  --json \
  "$RESULT_BUNDLE" > "$RESULT_DIR/coverage.json"

test -s "$RESULT_DIR/coverage.json"

파서는 먼저 targets가 존재하는지 확인한 뒤 대상별로 파일을 집계해야 합니다. 보고서가 비어 있다면 이를 커버리지 0%로 간주해 비교를 계속하지 말고, 수집 실패로 즉시 표시하세요. 흔한 원인으로는 scheme에서 테스트가 활성화되지 않은 경우, 대상이 커버리지 집계에 포함되지 않은 경우, 보고서가 저장되기 전에 테스트 프로세스가 비정상 종료된 경우가 있습니다.

원본 증거 보관

다음 항목을 동일한 작업의 산출물로 보관하는 것이 좋습니다.

산출물 용도
TestResults.xcresult 테스트, 로그와 커버리지 출처 검토
coverage.json 스크립트가 안정적으로 파싱할 입력 제공
coverage-summary.json 임계값, 실제 값과 실패 파일 저장
command.txt 실행 매개변수와 도구 체인 재현

백분율 스크린샷만 업로드해서는 ‘실제로 어떤 테스트가 실행되었는가’에 답할 수 없습니다. 문제를 조사하려면 원본 결과 번들이 필요합니다.

두 단계 게이트 동시 적용

프로젝트 전체 커버리지는 큰 폭의 회귀를 찾는 데 적합합니다. 하지만 대규모 코드베이스에 테스트되지 않은 코드 수십 줄이 추가되더라도 전체 값은 0점 몇 퍼센트포인트만 변할 수 있습니다. 따라서 다음 두 가지 규칙을 함께 사용하는 것이 좋습니다.

  1. 프로젝트 전체 커버리지는 고정된 하한선보다 낮아서는 안 됩니다.
  2. 이번 변경에 포함된 실행 가능한 소스 파일에는 더 높은 임계값을 적용해야 합니다.

예를 들어 전체 커버리지 하한선은 72%, 변경 파일 임계값은 85%로 설정할 수 있습니다. 임계값은 한 번에 달성하기 어려운 이상적인 수치가 아니라 팀의 현재 기준선에서 정해야 합니다. 아래 Python 코드는 대상 커버리지를 읽는 방법을 보여 줍니다. 실제 프로젝트에서는 files를 펼쳐 파일별 판단을 추가할 수 있습니다.

import json
import sys

with open("Artifacts/Coverage/coverage.json", encoding="utf-8") as f:
    report = json.load(f)

targets = report.get("targets", [])
if not targets:
    raise SystemExit("Coverage report has no targets")

tested = [t for t in targets if t.get("name") == "ExampleApp.app"]
if len(tested) != 1:
    raise SystemExit("Expected application target was not found")

coverage = float(tested[0]["lineCoverage"]) * 100
minimum = 72.0

print(f"application_line_coverage={coverage:.2f}")
sys.exit(0 if coverage >= minimum else 2)

스크립트는 ‘커버리지가 기준에 미달한 경우’와 ‘보고서를 파싱할 수 없는 경우’에 서로 다른 종료 코드를 사용해야 합니다. 전자는 테스트를 보강하거나 변경 사유를 설명해야 하고, 후자는 수집 경로를 수정해야 합니다. 두 상황을 같은 실패 유형으로 처리해서는 안 됩니다.

실제로 테스트가 필요한 파일만 검사

변경된 파일은 git diff --name-only로 가져온 뒤 xccov 보고서의 경로와 교집합을 구할 수 있습니다. 모든 .swift 파일을 무조건 게이트에 포함하지 마세요. 일반적으로 다음 항목은 명시적으로 제외해야 합니다.

제외 규칙은 저장소에 넣고 코드 리뷰를 거쳐야 합니다. 실패한 뒤 임시로 와일드카드 규칙을 추가해서는 안 됩니다. 경로를 비교하기 전에는 저장소 루트를 기준으로 통일하고, 심볼릭 링크를 해석하며, 임시 빌드 디렉터리 접두사를 제거해야 합니다. 그렇지 않으면 같은 파일도 절대 경로가 다르다는 이유로 일치하지 않을 수 있습니다.

이름이 변경된 파일에는 단순한 파일 목록보다 git diff --name-status -M이 더 안정적입니다. 파일이 이전 경로에서 새 경로로 이동했다면 새 경로를 기준으로 보고서 항목을 찾아야 하며, ‘보고서에 없는 파일’로 잘못 판단해서는 안 됩니다.

병렬 테스트와 변동성 제어

간헐적인 커버리지 변화는 대개 xccov 자체의 무작위성 때문이 아니라 테스트 실행의 비결정성 때문에 발생합니다. 비동기 테스트는 고정된 시간만큼 대기하지 말고 명확한 상태가 될 때까지 기다려야 합니다. 공유 데이터베이스, 싱글턴과 임시 디렉터리는 각 테스트 전에 초기화하세요. 병렬 테스트가 동일한 리소스를 두고 경합한다면 먼저 관련 테스트 대상의 병렬 실행을 끈 뒤 구체적인 공유 지점을 찾아야 합니다.

시뮬레이터의 수명 주기도 추적할 수 있어야 합니다. 장기 실행기는 작업 전에 앱 데이터를 정리할 수 있지만 매번 모든 런타임을 삭제할 필요는 없습니다. 실제로 비교해야 하는 것은 같은 도구 체인에서 동일한 테스트 계획을 연속 실행했을 때 일관된 결과가 나오는지 여부입니다. 먼저 동일한 커밋을 세 번 반복 실행하고 각 파일의 차이를 기록하세요. 계속 변동하는 파일은 테스트 격리를 먼저 수정한 뒤 엄격한 게이트에 포함해야 합니다.

마지막으로 실패 요약에는 개발자가 즉시 조치할 수 있는 정보를 담으세요. 실제 전체 커버리지, 요구값, 임계값보다 낮은 변경 파일, 해당 파일에서 커버된 라인 수와 실행 가능한 라인 수, 원본 xcresult의 보관 위치를 포함해야 합니다. 그러면 개발자는 전체 작업을 다시 실행하지 않고도 테스트를 보강할지, 스크립트를 수정할지, 테스트 환경의 불확실성을 해결할지 판단할 수 있습니다.

자주 묻는 질문

프로젝트 전체 커버리지만 검사해도 충분한가요?

충분하지 않습니다. 전체 수치는 큰 하락을 찾는 데 적합하지만 작은 변경에는 둔감합니다. 변경 파일 기준을 함께 검사하고 생성 코드, 리소스 접근 코드, 테스트 보조 파일은 명시적으로 제외해야 합니다.

같은 커밋의 커버리지가 가끔 달라지는 이유는 무엇인가요?

테스트 선택 차이, 끝나지 않은 비동기 작업, 병렬 테스트의 공유 상태, 시뮬레이터 잔여 데이터, Xcode 버전 차이가 주요 원인입니다. scheme, destination, 테스트 계획과 도구 버전을 먼저 고정하세요.

게이트 실패 시 어떤 산출물을 보관해야 하나요?

원본 xcresult, coverage.json, 임계값 판정 요약, 실제 실행 명령과 커밋 식별자를 보관하세요. 테스트 누락, 파싱 오류, 실제 커버리지 하락을 구분하는 데 필요합니다.

전용 물리 노드

프로젝트에 따라 켜고 끄는 클라우드 Mac으로 워크플로 재현하기

세 가지 Apple Silicon 구성 중 적합한 리소스를 선택하고 일·주·월·분기 단위로 대여하세요. 가상 머신이 아닌 전용 물리 장비이며, 실제 사용 가능 상태는 콘솔에서 실시간으로 확인할 수 있습니다.

구성 선택 후 주문하기