사용자가 클라우드 Mac에서 아직 제출하지 않은 양식을 작성하다가 자료를 확인하려고 다른 앱으로 전환한 사이, 몇 분 뒤 시스템이 프로세스를 종료할 수 있습니다. 앱을 다시 열었을 때 가장 심각한 문제는 충돌이 아닙니다. 화면은 정상처럼 보이지만 초안이 사라지고, 탐색 위치가 홈으로 돌아가거나, 이미 유효하지 않은 객체가 복원되는 상황입니다. 이런 문제는 일반적인 단위 테스트로 다루기 어려우므로 독립된 시뮬레이터에서 XCUITest로 전체 수명 주기를 재현하는 방식이 더 적합합니다.
복원해야 할 상태부터 정의하기
“마지막 화면 복원”을 하나의 요구사항으로 취급해서는 안 됩니다. 테스트를 작성하기 전에 상태를 다음 세 가지 유형으로 구분합니다.
| 상태 유형 | 예시 | 재실행 후 기대 결과 |
|---|---|---|
| 비즈니스 상태 | 초안 텍스트, 필터 조건, 선택한 항목 | 제품 규칙에 따라 복원 |
| 탐색 상태 | 현재 화면, 탭, 목록 위치 | 여전히 유효한 계층으로 복원 |
| 임시 상태 | 로딩 애니메이션, 팝업, 일회용 토큰 | 폐기한 뒤 다시 계산 |
복원 로직에서는 정상적인 백그라운드 전환과 비정상 종료도 구분해야 합니다. 전자는 화면의 세부 상태를 더 많이 유지할 수 있지만, 후자는 데이터 일관성을 우선해야 합니다. 초안이 이미 삭제된 객체에 의존한다면 유효하지 않은 화면을 억지로 재구성하는 대신, 안전한 화면으로 돌아가 그 이유를 사용자에게 알려야 합니다.
상태 복원 테스트의 목표는 모든 픽셀을 원래 위치로 되돌리는 것이 아니라, 사용자가 작업을 계속할 수 있게 하고 오래되었거나 권한이 없거나 제출할 수 없는 데이터가 노출되지 않도록 보장하는 것입니다.
각 시나리오에는 “준비 상태, 종료 동작, 복원 검증, 나타나서는 안 되는 상태”의 네 가지 항목을 작성하는 것이 좋습니다. 그러면 테스트가 실패했을 때 팀에서 원인이 영속화, 라우팅, 데이터 검증 중 어디에 있는지 판단할 수 있습니다.
자동화를 위한 결정적 진입점 준비하기
UI 테스트가 이전 실행에서 남은 데이터에 의존해서는 안 됩니다. 앱은 테스트 전용 실행 인수가 전달된 경우 고정 픽스처를 가져와 별도 컨테이너에 데이터를 저장할 수 있습니다. 픽스처 이름은 변경되기 쉬운 데이터베이스 기본 키가 아니라 비즈니스 시나리오를 나타내야 합니다.
let arguments = ProcessInfo.processInfo.arguments
if arguments.contains("-ui-testing"),
let index = arguments.firstIndex(of: "-fixture"),
arguments.indices.contains(index + 1) {
let fixture = arguments[index + 1]
try TestFixtureLoader.load(named: fixture)
}
프로덕션 빌드에서는 이 테스트 진입점을 무시하거나 제거해야 합니다. 더 안전한 방법은 컴파일 조건을 사용해 로더를 내부 테스트 구성에서만 활성화하는 것입니다. 테스트를 시작하기 전에는 알림, 캐시, 공유 환경설정도 삭제해야 합니다. “기존 초안이 우연히 남아 있는” 탓에 테스트가 잘못 통과하는 일을 막기 위해서입니다.
Runner의 전용 물리 노드에서 실행하더라도 각 파이프라인에는 독립적인 시뮬레이터 디바이스 세트를 할당해야 합니다. 전용 컴퓨팅 리소스가 테스트 데이터의 자동 격리를 의미하지는 않습니다. 두 개의 동시 작업이 동일한 시뮬레이터를 공유하면 앱 컨테이너를 서로 덮어쓸 수 있습니다.
XCUITest로 프로세스 종료와 재실행 재현하기
다음 테스트는 먼저 초안을 만든 다음 앱을 백그라운드로 보내고, 프로세스를 종료한 뒤 다시 실행합니다. 모든 핵심 컨트롤에는 안정적인 accessibility identifier를 지정해 현지화된 문구로 요소를 찾는 방식을 피해야 합니다.
func testDraftRestoresAfterTermination() {
let app = XCUIApplication()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project",
"-reset-state", "YES"
]
app.launch()
app.buttons["project.create"].tap()
let editor = app.textViews["draft.editor"]
editor.tap()
editor.typeText("release checklist")
app.buttons["draft.save"].tap()
XCUIDevice.shared.press(.home)
app.terminate()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project"
]
app.launch()
XCTAssertTrue(app.navigationBars["draft.screen"].waitForExistence(timeout: 8))
XCTAssertEqual(app.textViews["draft.editor"].value as? String,
"release checklist")
XCTAssertTrue(app.staticTexts["restoration.completed"].exists)
}
동일한 테스트의 두 번째 실행에서는 재설정 인수를 다시 전달하지 마세요. 검증해야 할 상태를 테스트 자체가 삭제하게 됩니다. 편집기의 존재 여부만 검증해서도 안 됩니다. 내용, 탐색 제목, 선택된 객체, 복원 안내도 함께 확인해야 합니다.
세 가지 실행 경로를 분리해 테스트하기
최소한 다음 세 가지 독립 테스트 케이스를 유지합니다.
- 백그라운드에서 바로 포그라운드로 복귀해 잠시 앱을 벗어난 것만으로 전체 화면이 재구성되지 않는지 검증합니다.
- 상태를 저장한 뒤 프로세스를 종료해 재실행 후에도 작업을 계속할 수 있는지 검증합니다.
- 유효하지 않은 객체를 만든 뒤 재실행해 앱이 안전하게 폴백하고 잘못된 경로를 삭제하는지 검증합니다.
각 테스트 케이스에서는 하나의 수명 주기만 검증해야 합니다. 그러면 십여 개 단계를 포함한 긴 테스트보다 실패 원인을 훨씬 직접적으로 찾을 수 있습니다.
클라우드 Mac에서 시뮬레이터와 실행 디렉터리 격리하기
자동화 작업에서는 로그인 사용자의 기본 디렉터리에 있는 시뮬레이터를 사용하지 말고 임시 디바이스 세트를 명시적으로 생성해야 합니다. 작업이 끝난 뒤 디바이스 세트를 삭제하면 앱 컨테이너와 남은 스냅샷도 함께 정리할 수 있습니다.
set -euo pipefail
DEVICE_SET="$RUNNER_TEMP/CoreSimulator"
RESULTS="$RUNNER_TEMP/StateRestoration.xcresult"
mkdir -p "$DEVICE_SET"
xcrun simctl --set "$DEVICE_SET" create \
"State-Restore-iPhone" \
"com.apple.CoreSimulator.SimDeviceType.iPhone-16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0"
UDID="$(xcrun simctl --set "$DEVICE_SET" list devices \
available -j | /usr/bin/python3 scripts/first_device.py)"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleUITests \
-destination "platform=iOS Simulator,id=$UDID" \
-resultBundlePath "$RESULTS"
xcrun simctl --set "$DEVICE_SET" shutdown "$UDID" || true
rm -rf "$DEVICE_SET"
예시의 런타임과 디바이스 유형은 노드에 설치된 버전으로 바꿔야 합니다. 먼저 xcrun simctl list runtimes와 xcrun simctl list devicetypes를 실행해 확인할 수 있습니다. 스크립트가 “최신” 런타임을 자동으로 선택하게 두지 마세요. Xcode가 업데이트되면 스크린샷, 시스템 팝업, 복원 동작이 모두 함께 달라질 수 있습니다.
실패 증거를 진단 가능한 결과로 만들기
상태 복원 실패는 대부분 두 번째 실행 이후에 발생하므로 복원 전후의 증거를 보존해야 합니다. 핵심 동작이 끝날 때마다 스크린샷을 첨부하고, 테스트에 사용한 픽스처 이름, 시뮬레이터 식별자, 앱 버전, 복원 단계를 XCTest attachment에 기록하는 방식을 권장합니다. 실패한 경우에는 .xcresult를 보존하고, 성공한 작업의 보존 기간은 팀 정책에 따라 줄일 수 있습니다.
문제 해결 순서는 다음과 같이 고정할 수 있습니다.
- 첫 번째 실행에서 상태가 실제로 저장되었는지 확인합니다.
- 백그라운드 진입 시 원자적 저장이 완료되었는지 확인합니다.
- 두 번째 실행에서 정리 로직이 다시 실행되지 않았는지 확인합니다.
- 영속화된 데이터의 버전 마이그레이션이 성공했는지 확인합니다.
- 마지막으로 라우터가 복원된 객체를 허용하는지 확인합니다.
초안은 복원되었지만 화면이 홈으로 돌아갔다면 탐색 재구성에 문제가 있을 가능성이 큽니다. 화면은 올바르지만 필드가 비어 있다면 영속화 시점과 저장소 키를 확인해야 합니다. 병렬 작업에서만 실패한다면 시뮬레이터, 결과 디렉터리, 픽스처가 실제로 격리되어 있는지부터 점검합니다.
이러한 시나리오를 병합 게이트에 포함하면 상태 복원을 확인하기 위해 사람이 반복해서 앱을 전환할 필요가 없습니다. 영속화 모델, Scene 수명 주기 또는 탐색 구조가 변경될 때마다 파이프라인에서 동일한 입력을 사용해 사용자가 중단된 지점부터 계속 작업할 수 있는지 검증할 수 있습니다.
자주 묻는 질문
상태 복원 테스트에 개발자가 평소 쓰는 시뮬레이터를 사용해도 되나요?
권장하지 않습니다. 자동화 전용 시뮬레이터나 별도 디바이스 세트를 만들고 각 시나리오 전에 앱 데이터를 초기화해야 이전 상태로 인한 오탐을 막을 수 있습니다.
앱이 다시 실행되는지만 확인하면 충분하지 않은 이유는 무엇인가요?
재실행 성공은 충돌이 없다는 사실만 보여 줍니다. 초안 내용, 탐색 단계, 선택 항목과 복원 안내를 함께 검증하고 민감한 임시 데이터가 남지 않았는지도 확인해야 합니다.
클라우드 M4 Mac에서 다음 작업 실행
Runner M4 또는 Runner M4 Plus를 선택하고 프로젝트에 맞춰 노드, 대여 기간 및 스토리지 옵션을 결정하세요. 각 주문은 독립된 물리 머신에 배정되며 가상 머신이 아닙니다.