클라우드 Mac의 Xcode 테스트 계획 일관성 검증

클라우드 Mac의 Xcode 테스트 계획 일관성 검증

팀에서 단위 테스트, API 테스트, UI 테스트를 여러 Xcode Test Plan으로 분리하고 나면 가장 흔한 문제는 테스트 실패가 아니라 테스트 자체가 예상대로 실행되지 않는 것이다. 누군가는 로컬 Scheme에서 기본 계획을 바꾸고, 누군가는 특정 테스트를 임시로 건너뛴 뒤 파일을 커밋하며, 또 다른 누군가는 환경 변수를 수정하고도 파이프라인에는 반영하지 않는다. 클라우드 Mac은 저장소에 들어 있는 설정을 그대로 실행한다. 따라서 가장 먼저 할 일은 재시도 횟수를 늘리는 것이 아니라 테스트 계획 자체를 검토가 필요한 코드로 다루는 것이다.

추적 가능한 단일 진입점 고정하기

Scheme은 반드시 공유 파일이어야 하며, 일반적인 경로는 App.xcodeproj/xcshareddata/xcschemes/App-CI.xcscheme이다. 사용자 디렉터리 아래의 xcuserdata에 의존해서는 안 된다. 그곳의 설정은 버전 관리 저장소에 안정적으로 포함되지 않는다.

Scheme의 Test Action에서 저장소에 있는 App-CI.xctestplan을 참조하고, 파이프라인에서는 계획 이름을 명시적으로 전달한다. 빌드 노드가 작업을 시작할 때는 먼저 해당 계획이 표시되는지 확인한다.

set -euo pipefail

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App-CI \
  -showTestPlans

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App-CI \
  -testPlan App-CI \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  test

-testPlan은 생략하면 안 된다. 생략할 경우 Scheme의 기본값이 바뀌어도 명령은 계속 성공할 수 있지만, 실제로 실행되는 테스트 집합은 이미 달라졌을 수 있다.

계획 파일을 검증 가능한 계약으로 만들기

.xctestplan은 JSON 파일이므로 정적 검사에 적합하다. 게이트에서는 최소한 다음 네 가지 필드 유형을 검사해야 한다.

검사 항목 필수 규칙 실패가 의미하는 것
configurations CI 구성을 반드시 포함해야 함 파이프라인 진입점이 삭제되거나 이름이 변경됨
testTargets 합의된 대상을 반드시 포함해야 함 특정 테스트 그룹이 실행되지 않음
skippedTests 허용 목록에 있는 항목만 포함할 수 있음 사유가 설명되지 않은 제외 항목이 추가됨
environmentVariableEntries 민감한 값은 금지하고 필수 키는 반드시 존재해야 함 환경이 달라졌거나 정보가 저장소에 들어감

JSON 전체의 텍스트 순서를 직접 비교해서는 안 된다. Xcode가 배열 순서를 바꾸거나 필드를 추가할 수 있으므로, 문자 단위 비교는 의미 없는 실패를 만든다. 구조를 읽은 뒤 팀이 실제로 의존하는 제약만 검증해야 한다.

테스트 계획 게이트의 목적은 모든 변경을 막는 것이 아니라, “어떤 테스트가 빠졌는지, 어떤 환경이 바뀌었는지”를 병합 전에 드러내는 데 있다.

스크립트로 누락된 대상과 제외된 테스트 차단하기

다음 스크립트는 CI 구성, 필수 테스트 대상, 등록되지 않은 제외 항목을 검사한다. 허용 목록은 매우 짧게 유지해야 하며, 코드 리뷰에서는 각 항목을 제거할 조건을 반드시 설명하도록 해야 한다.

#!/usr/bin/env python3
import json
import sys
from pathlib import Path

plan = json.loads(Path("App-CI.xctestplan").read_text())
required_targets = {"AppTests", "AppIntegrationTests"}
allowed_skips = {
    "AppIntegrationTests/testTemporaryServerResponse"
}

config_names = {item["name"] for item in plan.get("configurations", [])}
if "CI" not in config_names:
    sys.exit("Missing CI test configuration")

targets = plan.get("testTargets", [])
target_names = {
    item.get("target", {}).get("name")
    for item in targets
}
missing = required_targets - target_names
if missing:
    sys.exit(f"Missing test targets: {sorted(missing)}")

actual_skips = {
    test
    for item in targets
    for test in item.get("skippedTests", [])
}
unexpected = actual_skips - allowed_skips
if unexpected:
    sys.exit(f"Unapproved skipped tests: {sorted(unexpected)}")

이 스크립트는 테스트 명령보다 먼저 실행한다. 검사에 실패하더라도 계획 파일을 자동으로 다시 작성해서는 안 된다. 자동 수정이 개발자가 의도적으로 적용한 설정 변경을 가릴 수 있기 때문이다.

허용 목록에 만료 조건 설정하기

허용 목록이 예외를 영구 보관하는 쓰레기통이 되어서는 안 된다. 각 항목에는 최소한 관련 결함 기록, 담당자, 삭제 조건이 있어야 한다. 팀에서 별도의 구조화 파일을 더 관리하고 싶지 않다면 코드 리뷰 템플릿에 이 정보를 필수로 입력하게 하고, 스크립트를 정기적으로 실행해 현재 목록을 출력할 수 있다.

안정적인 변수와 파이프라인 비밀 분리하기

테스트 계획에는 노드에 따라 달라지지 않는 설정을 저장하는 것이 적합하다. 예를 들어 UITEST_MODE=1, 고정 언어, 모의 서비스 모드가 이에 해당한다. 액세스 토큰, 개인 키, 일회성 자격 증명은 계획 파일에 기록해서는 안 되며, Scheme의 평문 인수로 전달해서도 안 된다.

클라우드 Mac의 파이프라인은 실행 전에 비밀을 주입하고 테스트 프로세스가 환경 변수로 읽게 할 수 있다. 감사 스크립트는 키가 존재하는지, 값이 허용된 고정 집합에 속하는지만 검증한다. 이렇게 하면 계획의 재현성을 유지하면서 민감한 정보가 저장소에 섞이는 것을 막을 수 있다.

변수가 어떤 대상을 기준으로 확장되는지도 확인해야 한다. 계획에서 이미 이름이 변경된 Target을 참조하는 경우 Xcode 화면에서는 파일이 계속 열릴 수 있지만, 실행 시 변수 해석은 예상과 달라질 수 있다. 프로젝트 이름을 변경할 때는 계획 검사와 xcodebuild -list를 함께 실행해야 한다.

실패 원인을 판단할 충분한 증거 남기기

게이트를 통과한 뒤 전체 테스트를 실행하고 결과 번들을 고정된 디렉터리에 기록한다.

rm -rf artifacts/App-CI.xcresult

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App-CI \
  -testPlan App-CI \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath artifacts/App-CI.xcresult \
  test

실패하면 세 가지 정보를 보관해야 한다. 현재 .xctestplan, 실제로 실행한 명령, .xcresult다. 콘솔 로그의 마지막 부분만 저장해서는 테스트 로직이 실패한 것인지, 대상이 로드되지 않은 것인지, 계획 설정이 바뀐 것인지 판단하기 어렵다.

ZoneMini에서 고정 테스트 노드를 실행할 때도 장기간 실행 중인 환경이 언제나 동일하다고 가정하지 말고, 각 작업을 시작하기 전에 정적 감사를 다시 수행해야 한다. 공유 Scheme은 진입점을 정하고, 테스트 계획은 실행할 집합을 설명하며, 감사 스크립트는 변경을 제한하고, 결과 번들은 증거를 보존한다. 이 네 계층이 모두 갖춰져야만 “테스트 통과”가 일관된 의미를 가질 수 있다.

자주 묻는 질문

CI에서 xcodebuild 명령만 고정하면 왜 충분하지 않나요?

명령은 Scheme과 테스트 계획 이름만 고정합니다. 계획 파일 내부의 테스트 대상, 제외 항목, 환경 변수는 별도로 바뀔 수 있으므로 파일 내용 감사가 필요합니다.

skippedTests를 모두 금지해야 하나요?

그럴 필요는 없습니다. 임시 제외가 필요하면 담당자, 사유, 제거 조건이 있는 허용 목록에 등록하고, 등록되지 않은 새 제외 항목만 병합을 차단합니다.

개발 Mac과 클라우드 Mac이 같은 테스트 계획을 선택하게 하려면 어떻게 하나요?

Scheme을 xcshareddata/xcschemes에 저장하고 .xctestplan을 버전 관리한 뒤 CI에서 -testPlan을 명시합니다. 실행 전 -showTestPlans로 노출 여부도 확인합니다.

전용 빌드 슬롯

지속적인 빌드 작업에 사용할 독점 클라우드 Mac 선택

M4, 메모리, 스토리지, 노드 및 결제 주기를 확인한 뒤 기존 파이프라인에 고정 빌드 환경을 연결하세요.

구성 선택 및 주문