一名使用者在 App 中儲存了文件,卻無法從系統搜尋結果開啟。相同測試在開發用 Mac 上能通過,在雲端 Mac 上卻偶爾失敗:索引寫入回呼已回報成功,搜尋仍找不到結果。要定位這類問題,不能把「提交成功」、「可以搜尋」和「移除後不再出現」合併成一項斷言;它們是索引生命週期中必須分別驗收的三個階段。
先界定測試範圍
Core Spotlight 的 CSSearchableIndex 負責提交和移除 App 提供的可搜尋項目。寫入回呼回報的是該次操作的結果,不代表搜尋查詢已讀到新資料。因此,測試至少要記錄項目識別碼、搜尋詞、寫入結果、首次命中的時間,以及移除後的查詢結果。
測試應在以 App 為宿主的 XCTest 目標中執行,而不是只執行不載入 App 環境的邏輯測試目標。測試項目使用獨立的 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。
最後檢查並行執行的安排:同一 App 的索引生命週期測試若共用網域清理操作,就不應在同一台模擬器上平行執行。保留每次執行的識別碼、三個階段的結果與測試結果套件,才能區分操作錯誤、搜尋同步延遲,以及測試自身造成的狀態污染。只有寫入、搜尋和移除都通過,這條使用者搜尋流程才算完成驗收。
常見問題
寫入回呼成功後,為何立刻搜尋仍可能找不到項目?
回呼確認寫入操作完成,並不保證搜尋端已同步。請用唯一測試詞限時重試,逾時後再判定失敗。
如何避免不同測試批次的索引資料互相干擾?
每次執行產生獨立識別碼與搜尋詞,在開始前及結束時刪除該項目;失敗時記錄識別碼與操作階段。
將固定規格的雲端 Mac 整合至你的工作流程
可按日、週、月或季租用獨享實體節點,以固定硬體資源執行建置、自動化與 MLX 推論工作。