동일한 UI 테스트가 로컬에서는 통과하지만 원격 노드에서는 간헐적으로 시간 초과된다면, 원인은 대개 비즈니스 로직이 아니라 시뮬레이터의 잔여 상태, 달라진 테스트 분할 경계 또는 과도한 동시 실행에 있습니다. 신뢰할 수 있는 테스트 매트릭스라면 실행할 때마다 세 가지 질문에 답할 수 있어야 합니다. 어떤 런타임을 사용했는지, 어떤 테스트 사례를 한 그룹으로 실행했는지, 실패 당시의 자료를 어디에 저장했는지입니다. 아래에서는 xcodebuild와 simctl을 사용해 반복 실행 가능한 워크플로를 구축합니다.
먼저 매트릭스 범위 정의
처음부터 모든 기기와 OS 버전을 순회하지 마세요. 우선 주력 런타임 하나와 대표 기기 하나로 안정적인 기준선을 확보한 다음 호환성 조합을 추가합니다. 실행 전에는 Xcode 버전, macOS 버전, 런타임 식별자, 커밋, 테스트 목록을 기록합니다. 같은 이름의 시뮬레이터가 동시에 존재할 수 있으므로 기기 이름은 고유 키로 적합하지 않습니다. 대신 UDID를 저장해야 합니다.
| 구분 | 기준선 선택 | 확장 조건 |
|---|---|---|
| OS 런타임 | 현재 프로젝트가 지원하는 주력 버전 | 출시 전 최소 지원 버전까지 검증 |
| 기기 유형 | 자주 사용하는 화면 크기 하나 | 레이아웃 대응을 검증할 때 크기 추가 |
| 테스트 분할 | 테스트 클래스별 고정 그룹 | 단일 그룹의 실행 시간이 지나치게 길 때 추가 분할 |
| 동시 실행 수 | 두 개의 분할부터 시작 | 리소스 사용 추이가 안정된 후 점진적으로 증가 |
테스트 매트릭스의 목표는 가능한 한 많은 조합을 만드는 것이 아니라, 동일한 커밋과 동일한 입력에서 설명 가능한 결과를 얻는 것입니다.
일회용 시뮬레이터 기준선 생성
먼저 노드에 실제로 설치된 런타임과 기기 유형을 조회합니다. 스크립트에 버전 번호를 하드 코딩하지 마세요.
xcrun simctl list runtimes
xcrun simctl list devicetypes
xcrun simctl list devices available
출력에서 알맞은 식별자를 선택한 다음 각 분할마다 독립된 기기를 생성합니다. 이름은 식별하기 쉽게 붙이는 용도일 뿐이며, 이후 명령에서는 모두 UDID를 사용합니다.
RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-XX-X"
DEVICE_TYPE_ID="com.apple.CoreSimulator.SimDeviceType.iPhone-XX"
UDID=$(xcrun simctl create "ui-shard-01" "$DEVICE_TYPE_ID" "$RUNTIME_ID")
xcrun simctl boot "$UDID"
xcrun simctl bootstatus "$UDID" -b
개발자가 일상적인 디버깅에 사용하는 시뮬레이터를 재사용하지 마세요. 테스트는 권한, 지역 설정, 키보드, 앱 데이터, 백그라운드 작업을 변경할 수 있습니다. 일회용 기기를 사용하면 이러한 상태를 한 번의 실행 범위 안으로 제한할 수 있습니다. 테스트 사례가 카메라, 알림 또는 위치 권한에 의존한다면 이전 실행의 권한이 그대로 남아 있다고 가정하지 말고 테스트 준비 단계에서 명시적으로 설정해야 합니다.
한 번 빌드한 뒤 목록에 따라 분할
각 분할에서 동일한 프로젝트를 반복해서 빌드하면 안 됩니다. 먼저 build-for-testing을 실행해 공통 테스트 산출물을 생성한 다음 각 분할에서 test-without-building을 호출합니다.
DERIVED_PATH="$PWD/.derived-test"
xcodebuild build-for-testing \
-workspace App.xcworkspace \
-scheme AppUITests \
-destination "generic/platform=iOS Simulator" \
-derivedDataPath "$DERIVED_PATH"
XCTESTRUN=$(find "$DERIVED_PATH" -name "*.xctestrun" -print -quit)
분할 목록은 저장소에 커밋해야 합니다. 알파벳순이나 실행 시간에 따라 임시로 나누면 실행할 때마다 경계가 달라질 수 있으므로 피해야 합니다. 예를 들어 로그인과 로그아웃은 같은 그룹에, 오프라인과 동기화는 다른 그룹에 배치합니다. 실행할 때는 Xcode의 내장 2차 병렬화를 끄고, 동시 실행을 외부 스케줄러에서만 제어합니다.
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-destination "platform=iOS Simulator,id=$UDID" \
-only-testing:"AppUITests/AuthenticationTests" \
-parallel-testing-enabled NO \
-resultBundlePath "$PWD/results/shard-01.xcresult"
분할별 실행 시간 차이가 크다면 최근 여러 차례 실행의 중앙값을 기준으로 목록을 조정합니다. 단, 조정한 뒤에는 버전을 고정하고 변경 사항을 기록해야 합니다. 스케줄링 스크립트가 실행할 때마다 자동으로 테스트를 재배치하게 두지 마세요. 그러면 실패 결과를 직전 실행과 직접 비교할 수 없습니다.
동시 실행 제어 및 안정적인 정리
시뮬레이터는 많이 병렬 실행한다고 항상 빨라지는 것이 아닙니다. 분할을 하나 추가할 때마다 시작 프로세스, 그래픽 서비스, 테스트 프로세스, 앱 인스턴스가 함께 늘어납니다. 우선 두 개의 분할로 메모리 압력, CPU 포화도, 부팅 시간, 전체 실행 시간을 관찰하세요. 메모리 스왑이 발생하거나 bootstatus가 시간 초과되거나 테스트 사례의 실행 시간이 크게 흔들리면 즉시 동시 실행 수를 줄여야 합니다.
스크립트는 성공, 실패, 중단 여부와 관계없이 정리 작업을 수행해야 합니다. 생성한 UDID에 종료 처리기를 등록하고, 먼저 종료한 뒤 삭제해 다음 실행에서 오래된 기기를 잘못 재사용하지 않도록 합니다.
cleanup() {
xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$UDID" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
동일한 노드에서 다른 작업이 실행 중일 수 있으므로 전역 삭제 명령으로 노드의 모든 시뮬레이터를 제거하지 마세요. 현재 실행에서 생성하고 기록한 UDID만 처리해야 합니다. 실패 당시의 상태를 보존해야 한다면 로그와 결과 번들을 먼저 보관한 뒤 기기를 삭제합니다.
결과 번들로 실제 실패와 간헐적 실패 구분
각 분할에는 고유한 .xcresult 경로가 있어야 하며, 표준 출력, 시작 시간, 종료 시간, 종료 코드, UDID도 함께 저장해야 합니다. 경로를 중복 사용하면 가장 가치 있는 최초 실패 당시의 자료가 덮어써질 수 있습니다. 실패한 뒤에는 전체 매트릭스를 즉시 다시 실행하지 말고 해당 분할만 한 번 재실행합니다.
처음에는 실패했지만 다시 실행했을 때 통과한다면 대기 조건, 애니메이션, 네트워크 의존성 또는 상태 경쟁이 불안정할 가능성이 큽니다. 두 번 모두 같은 단계에서 실패한다면 결정적인 회귀일 가능성이 더 높습니다. 두 번의 실패 지점이 서로 다르다면 노드 리소스와 테스트 간 공유 상태를 우선 점검해야 합니다.
배포 전에는 다음 순서로 확인할 수 있습니다.
- 런타임과 기기 유형이 모두 현재 노드의 조회 결과에서 선택되었는지 확인합니다.
- 각 분할이 고유한 UDID와 독립된 결과 디렉터리를 사용하는지 확인합니다.
- 테스트 산출물을 한 번만 빌드하고 각 분할에서 다시 빌드하지 않는지 확인합니다.
- 테스트 목록에 버전 기록이 있으며 실행 전에 무작위로 재배치되지 않는지 확인합니다.
- 재실행으로 최초 실패 결과가 덮어써지지 않는지 확인합니다.
- 중단 후 현재 실행에서 생성한 시뮬레이터만 삭제하는지 확인합니다.
자주 묻는 질문
클라우드 Mac 한 대에서 시뮬레이터를 몇 개까지 동시에 실행해야 하나요?
두 개의 분할 작업으로 시작한 뒤 메모리 압력, CPU 사용률, 부팅 시간과 테스트 시간 편차를 확인해 늘리는 것이 안전합니다. 스왑이나 시간 초과가 생기면 동시 실행 수를 줄여야 합니다.
테스트 분할마다 별도 시뮬레이터가 필요한 이유는 무엇인가요?
앱 데이터, 권한, 클립보드와 백그라운드 프로세스를 분리해 다른 테스트의 상태가 섞이지 않도록 하기 위해서입니다.
테스트가 실패하면 전체를 다시 실행해야 하나요?
전체 재실행보다 최초 xcresult를 보존한 뒤 실패한 분할만 한 번 다시 실행하는 편이 좋습니다. 재실행에서 통과하면 불안정한 테스트로 분류해 두 결과를 비교합니다.
ArmMacs Cloud Mac
프로젝트 기간에 맞춰 전용 물리 노드 사용
고정 칩, 메모리, 스토리지, 대여 기간 및 판매 중인 노드를 선택하고, 재고가 있으면 제공 절차를 시작합니다.