사용자가 앱에서 문서를 저장했지만 시스템 검색 결과에서 열 수 없다. 개발용 Mac에서는 재현되지 않는데, 클라우드 Mac의 자동화 테스트에서는 간헐적으로 실패한다. 색인 등록 콜백은 성공했지만 검색 결과에는 아직 항목이 나타나지 않는다. 이런 문제를 찾으려면 ‘등록 성공’, ‘검색 가능’, ‘삭제 후 검색 결과에서 사라짐’을 하나의 단언으로 묶어서는 안 된다. 색인 수명 주기에서 각각 검증해야 하는 세 단계다.
테스트 범위부터 명확히 하기
Core Spotlight의 CSSearchableIndex는 앱이 제공하는 검색 가능 항목의 등록과 삭제를 담당한다. 등록 콜백이 알려주는 것은 해당 작업의 결과이지, 검색 쿼리가 새 데이터를 이미 읽을 수 있다는 뜻이 아니다. 따라서 테스트에는 최소한 항목 식별자, 검색어, 등록 결과, 최초 검색 성공 시각, 삭제 후 쿼리 결과를 기록한다.
테스트는 앱 환경을 로드하지 않는 로직 테스트 타깃만으로 실행하지 말고, 앱을 호스트로 사용하는 XCTest 타깃에서 실행한다. 테스트 항목에는 별도의 domainIdentifier를 사용하고, 실행할 때마다 새로운 uniqueIdentifier와 제목을 생성해 이전 실행에서 남은 항목으로 인한 거짓 양성을 방지한다. 먼저 항목 하나로 전체 흐름을 확인한 뒤 대량 등록으로 확대한다. 고정된 업무 문서를 테스트 데이터로 사용하지 않는다. 사용자가 문서를 수정하거나 삭제하면 테스트 결과도 달라질 수 있기 때문이다.
등록 콜백의 성공은 ‘등록 단계’만 통과했다는 뜻이다. 검색 단계에서는 실제로 쿼리를 실행하고, 삭제 단계에서는 다시 쿼리해야 한다. 세 단계의 결과를 각각 남긴다.
XCTest로 등록과 삭제 검증하기
다음 테스트는 비동기 작업을 XCTest에 연결하는 방법을 보여준다. 먼저 식별자로 남아 있을 수 있는 항목을 정리하고, 고유한 제목을 가진 항목 하나를 등록한 뒤 해당 식별자를 삭제한다. 이 예제는 작업 콜백을 검사하지만, 이 코드만으로 검색 결과에 항목이 나타났음을 증명하지는 못한다. 검색 단언은 다음 절의 방식으로 추가해야 한다.
import CoreSpotlight
import XCTest
final class SpotlightLifecycleTests: XCTestCase {
func testWriteAndRemove() {
let index = CSSearchableIndex.default()
let id = "smoke-\(UUID().uuidString)"
let title = "SpotlightSmoke-\(id)"
let attributes = CSSearchableItemAttributeSet(contentType: .text)
attributes.title = title
let item = CSSearchableItem(
uniqueIdentifier: id,
domainIdentifier: "spotlight-smoke-tests",
attributeSet: attributes
)
func complete(
_ name: String,
_ operation: (@escaping (Error?) -> Void) -> Void
) {
let done = expectation(description: name)
operation { error in
XCTAssertNil(error, "\(name): \(String(describing: error))")
done.fulfill()
}
wait(for: [done], timeout: 10)
}
complete("remove previous item") { finish in
index.deleteSearchableItems(
withIdentifiers: [id],
completionHandler: finish
)
}
complete("index item") { finish in
index.indexSearchableItems(
[item],
completionHandler: finish
)
}
complete("remove indexed item") { finish in
index.deleteSearchableItems(
withIdentifiers: [id],
completionHandler: finish
)
}
}
}
실제 프로젝트에서는 삭제 작업을 테스트 정리 경로에 넣어야 한다. 등록 단언이 도중에 실패하더라도 이미 제출한 항목의 삭제를 시도해야 한다. 정리 실패를 조용히 무시하지 말고 항목 식별자와 함께 보고해야 한다. 그렇지 않으면 남은 데이터가 이후 실행에 영향을 줄 수 있다.
검색 동기화에 제한 시간 두기
테스트 타깃에서 CSSearchQuery로 이번 실행에서 생성한 고유 제목이나 검색어를 조회하고, 반환된 항목의 uniqueIdentifier를 확인한다. 단순히 ‘결과가 하나 이상 있는지’만 검사해서는 안 된다. 같은 이름의 업무 데이터 때문에 잘못된 항목으로 테스트가 통과할 수 있다. 등록 콜백이 반환되면 쿼리를 시작한다. 결과가 없으면 잠시 기다렸다가 쿼리를 새로 생성해 재시도하고, 미리 정한 마감 시각까지 찾지 못했을 때만 실패 처리한다. 삭제 콜백 이후에도 같은 방식으로 해당 식별자가 더 이상 나타나지 않을 때까지 폴링한다.
| 단계 | 통과 조건 | 실패 시 우선 기록할 정보 |
|---|---|---|
| 등록 | 콜백에 오류가 없음 | 식별자, 오류, 시뮬레이터 상태 |
| 검색 | 마감 시각 전에 이번 실행의 식별자를 찾음 | 검색어, 쿼리 오류, 소요 시간 |
| 삭제 | 마감 시각 전에 해당 식별자가 더 이상 반환되지 않음 | 삭제 오류, 마지막 쿼리 결과 |
마감 시각은 테스트에 할당한 시간 예산이지 서비스 성능 보장이 아니다. 먼저 고정된 시뮬레이터 구성에서 여러 차례 실행해 본 뒤 합리적인 상한을 설정한다. 쿼리 한 번이 느렸다는 이유로 대기 시간을 무한히 늘리지 않는다. 쿼리 콜백과 폴링 타이머에도 각각 종료 조건을 두어 한 번의 작업 정체가 전체 파이프라인을 점유하지 않도록 한다.
클라우드 Mac에서 실행 환경 고정하기
이 테스트에 사용할 프로젝트, Scheme, 시뮬레이터 대상을 고정하고 Scheme이 공유되어 있는지 확인한다. 처음 실행하기 전에 xcodebuild -showdestinations로 현재 사용 가능한 대상을 확인한 다음, 선택한 기기의 식별자를 실행 설정에 넣는다. 자동으로 선택되는 ‘첫 번째’ 시뮬레이터에 의존하지 않는다. 예를 들면 다음과 같다.
xcodebuild test \
-project Example.xcodeproj \
-scheme Example \
-destination 'platform=iOS Simulator,id=SIMULATOR_UDID' \
-only-testing:ExampleTests/SpotlightLifecycleTests \
-resultBundlePath "$PWD/spotlight-result.xcresult"
SIMULATOR_UDID와 프로젝트 이름은 실제 값으로 바꾼다. 결과 번들 경로는 매 실행 전에 존재하지 않는 상태여야 한다. 색인 테스트를 위해 시뮬레이터 전체를 초기화할 필요는 보통 없다. 테스트 도메인과 이번 실행의 식별자를 우선 정리하면 실행 시간을 줄이면서 다른 테스트에 필요한 상태도 지킬 수 있다. 기기 초기화가 꼭 필요하다면 병렬 테스트와 기기 세트를 공유하지 않도록 초기화 단계를 별도 작업으로 분리한다.
간헐적 실패 조사하고 검증 마무리하기
실패하면 곧바로 재실행하기보다 어느 단계에서 실패했는지 먼저 확인한다. 등록 콜백에서 오류가 발생했다면 테스트 호스트, 항목 속성, 오류 객체를 살펴본다. 등록은 성공했지만 검색 시간이 초과됐다면 검색어가 실제로 검색 가능한 속성에 기록됐는지, 쿼리에서 이번 실행의 식별자를 확인하는지, 다른 작업이 같은 테스트 도메인을 정리하고 있지는 않은지 점검한다. 삭제 후에도 검색된다면 마지막으로 검색된 항목의 식별자를 기록하고, 삭제 호출이 동일한 uniqueIdentifier를 대상으로 했는지 확인한다.
마지막으로 동시 실행 구성을 점검한다. 같은 앱의 색인 수명 주기 테스트가 도메인 정리 작업을 공유한다면 동일한 시뮬레이터에서 병렬 실행해서는 안 된다. 실행마다 식별자, 세 단계의 결과, 테스트 결과 번들을 보관하면 작업 오류와 검색 동기화 지연, 테스트 자체의 상태 오염을 구분할 수 있다. 등록, 검색, 삭제를 모두 통과해야 사용자의 검색 경로를 검증했다고 할 수 있다.
자주 묻는 질문
등록 콜백이 성공했는데 검색 결과가 바로 나오지 않는 이유는 무엇인가요?
콜백은 등록 요청의 완료를 뜻하며 검색 색인의 동기화 완료를 보장하지 않습니다. 고유 검색어로 제한 시간 동안 재시도하세요.
테스트 실행 간 색인 데이터가 섞이지 않게 하려면 어떻게 하나요?
실행마다 고유 식별자와 검색어를 만들고 시작 전과 종료 시 해당 항목을 삭제합니다. 실패 기록에는 식별자와 실패 단계를 남기세요.
고정 구성의 클라우드 Mac을 워크플로에 연결하세요
일·주·월·분기 단위로 전용 물리 노드를 임대하고, 고정된 하드웨어 리소스로 빌드, 자동화 및 MLX 추론 작업을 실행하세요.