一名用户在应用里保存了文档,却无法从系统搜索结果打开它。开发机上重试正常,云端 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 推理任务。