Highlight
Apple 推出 AppIntentsTesting 框架,让开发者可以在 XCUITest 中通过 Bundle ID 跨进程执行真实的 App Intents,验证 Intent 执行、Entity 查询、Spotlight 索引和 View Annotation,全程不走 Mock。
核心内容
以前测 App Intents 有多麻烦
你的 App 接入了 Siri、Shortcuts、Spotlight,用户可以通过语音或快捷指令操作你的 App。但测试这些能力一直是个难题。
选项一:写 UI 测试去点 Shortcuts 界面。按钮位置一变,测试就挂。运行一次要几十秒,CI 里极不稳定。
选项二:在代码里直接调用 perform()。这绕过了系统的参数解析、Entity 匹配和权限检查,测不到真实路径。
结果:很多团队干脆不测,等用户反馈才发现 Siri 听不懂、Spotlight 搜不到、快捷指令传错参数。
AppIntentsTesting 做了什么
Apple 把测试框架直接做进了系统。测试代码写在标准的 XCUITest bundle 里,通过 IntentDefinitions(bundleIdentifier:) 拿到 App 中定义的所有 Intent、Entity 和 Query。测试运行时,Intent 在 App 进程里真实执行,结果通过 IPC 返回到测试进程。
测试 Target 不导入 App 代码,只传 Bundle ID。这强制测试走真实的系统路由,参数解析、Entity 查找、权限检查一个不落。
代价是编译期没有类型检查。makeIntent(name:color:) 的参数名和类型没有代码补全,拼错只在运行时报错。Apple 认为这是值得的:用一点编写时的不便,换系统级集成测试的可靠性。
测试还能覆盖系统级集成
AppIntentsTesting 不只测 Intent 执行。它还能测 Spotlight 索引是否生效、View Annotation 是否传对了 Entity ID。这些是以前完全测不到的黑箱。
详细内容
执行第一个 Intent 测试
(06:48)
以 CometCal 示例 App 的 CreateCalendarIntent 为例,创建一个日历并断言返回值:
import AppIntentsTesting
func testCreateCalendar() async throws {
let definitions = IntentDefinitions(bundleIdentifier: "com.example.apple-samplecode.CometCal")
let createCalendar = definitions.intents["CreateCalendarIntent"]
let result = try await createCalendar.makeIntent(
name: "Occupy Saturn",
color: "red"
).run()
XCTAssertEqual(try result.value.title, "Occupy Saturn")
}
关键点:
IntentDefinitions通过 Bundle ID 初始化,不依赖 App 代码导入intents["CreateCalendarIntent"]通过字符串下标获取 Intent 定义makeIntent的参数名需与 App 中定义的一致,但没有编译器检查color: "red"是 AppEnum 的 raw string,框架自动做类型转换result.value通过 dynamic member lookup 访问返回的 Entity 属性- 测试在独立进程运行,App 在另一进程执行 Intent,状态不共享
测试 Entity 字符串查询
(12:25)
Shortcuts 和 Siri 搜索 Entity 时,会调用 EntityStringQuery。下面用测试驱动的方式实现它:
func testEventStringQuery() async throws {
let results = try await eventEntityDefinition
.entities(matching: "Cosmic Ray")
XCTAssertEqual(results.count, 1)
XCTAssertEqual(try results[0].title, "Cosmic Ray Calibration")
}
测试先写,运行后失败。然后实现查询:
struct EventEntityQuery: EntityStringQuery {
func entities(for identifiers: [EventEntity.ID]) async throws -> [EventEntity] {
// 通过 ID 查找
}
func suggestedEntities() async throws -> [EventEntity] {
// 返回建议列表
}
func entities(matching string: String) async throws -> [EventEntity] {
try calendarManager.fetchEvents()
.filter { $0.title.localizedCaseInsensitiveContains(string) }
.map(\.entity)
}
}
关键点:
entities(matching:)在设备上真实执行字符串查询- 返回的
EventEntity数组可用 dynamic member lookup 断言属性 - 先写测试、再实现查询,确保 TDD 流程完整
- 修复后 Shortcuts 中搜索同一事件,结果正确出现
链式调用多个 Intent
(15:42)
Shortcuts 的核心用法是把一个 Intent 的输出传给下一个 Intent。测试可以模拟这个流程:
func testCreateAndUpdateEvent() async throws {
let createResult = try await createEventDefinition.makeIntent(
title: "Asteroid Dodgeball Practice",
startDate: Date(),
isAllDay: false,
calendar: "Deep Space"
).run()
XCTAssertEqual(try createResult.value.title, "Asteroid Dodgeball Practice")
let updateResult = try await updateEventDefinition.makeIntent(
title: "Asteroid Dodgeball Rules Overview",
event: createResult.value
).run()
XCTAssertEqual(try updateResult.value.title, "Asteroid Dodgeball Rules Overview")
}
关键点:
calendar: "Deep Space"传入字符串,AppIntents 运行时自动调用CalendarEntity的EntityStringQuery匹配第一个结果event: createResult.value直接把上一个 Intent 返回的 Entity 传给下一个 Intent- 链式调用模拟了用户在 Shortcuts 中拖拽连接的行为
- 整个流程在一个测试里完成,创建、断言、更新、再断言
测试专用 Intent
(17:45)
测试需要独立、可重复的数据环境。用 isDiscoverable = false 和 #if DEBUG 创建只在测试中使用的 Intent:
#if DEBUG
struct SeedSampleEventsIntent: AppIntent {
static let isDiscoverable = false
func perform() async throws -> some IntentResult {
// 创建已知列表的事件
return .result()
}
}
#endif
关键点:
isDiscoverable = false阻止系统在任何地方暴露这个 Intent#if DEBUG确保 Release 包不包含测试代码- 可用于注入测试数据、跳转到任意页面、重置应用状态
- 即使 App 的 UI 改版,测试依然稳定
测试 Spotlight 索引
(20:27)
App 创建事件后应该自动索引到 Spotlight。用 spotlightQuery() 验证:
func testNewEventIndexedInSpotlight() async throws {
let before = try await eventEntityDefinition.spotlightQuery("Supernova Viewing Party")
XCTAssertTrue(before.isEmpty, "Event should not exist in Spotlight yet")
// 创建 "Supernova Viewing Party" 事件
let after = try await eventEntityDefinition.spotlightQuery("Supernova Viewing Party")
XCTAssertEqual(after.count, 1)
XCTAssertEqual(try after[0].title, "Supernova Viewing Party")
}
关键点:
spotlightQuery(_:)直接与 Spotlight 通信,返回匹配的 Entity 列表- 先断言创建前搜不到,再断言创建后能搜到,防止回归
- 这个测试能捕获”索引代码被意外注释掉”这类静默 Bug
测试 View Annotation
(22:33)
View Annotation 告诉 Siri 当前屏幕显示的是哪个 Entity。测错了会导致 Siri 回答”这是什么”时给出错误信息:
func testEventViewAnnotation() async throws {
try await openEventDefinition.makeIntent(target: "Morning Launch Briefing").run()
let app = XCUIApplication()
let title = app.staticTexts["Morning Launch Briefing"]
XCTAssertTrue(title.waitForExistence(timeout: 5))
let annotations = try await eventEntityDefinition.viewAnnotations()
XCTAssertEqual(annotations.count, 1, "Expected exactly one view annotation")
XCTAssertEqual(try annotations[0].entity.title, "Morning Launch Briefing")
}
关键点:
- 先用 Intent 打开页面,再用 XCUI 确认 UI 渲染正确
viewAnnotations()获取系统报告的当前屏幕 Entity 列表annotations[0].entity通过 dynamic member lookup 访问 Entity 属性- 这个测试曾帮演讲者发现把
calendar.id误传为event.id的 Bug
核心启发
1. 把 Siri 自动化测试写进 CI
你的 App 如果有 Siri 快捷指令,现在可以写自动化测试覆盖核心路径。选一个最常用的语音指令,用 makeIntent(...).run() 执行并断言结果。每次提交自动跑,Siri 相关的回归能被立刻发现。
入口 API:IntentDefinitions(bundleIdentifier:) + intents["IntentName"].makeIntent(...).run()
2. 给 Spotlight 索引加回归测试
如果你用 CoreSpotlight 索引 App 内容,写一个测试:创建内容前 spotlightQuery 返回空,创建后返回一条结果。这能防止索引逻辑被意外破坏。
入口 API:entityDefinition.spotlightQuery("keyword")
3. 用测试专用 Intent 管理测试数据
给测试写一个 SeedDataIntent,用 #if DEBUG 包裹。在 setUp() 里调用它注入固定数据。每个测试用例从零开始,不再依赖上一个测试留下的脏数据。
入口 API:static let isDiscoverable = false
4. 验证 Siri 屏幕感知的正确性
如果你的 App 支持”Siri,这是什么”这类上下文指令,写一个 View Annotation 测试。打开特定页面,断言 viewAnnotations() 返回正确的 Entity ID。
入口 API:entityDefinition.viewAnnotations()
5. 用 TDD 实现 Entity 查询
下次实现 EntityStringQuery 时,先写 entities(matching:) 的测试,运行看到失败,再实现查询逻辑。这比在 Shortcuts 里手动搜索验证快得多。
入口 API:entityDefinition.entities(matching: "search term")
关联 Session
- Build agentic apps with App Intents — 了解 App Intents 的新能力,为测试找到更多覆盖点
- What’s new in App Intents — 掌握 App Intents 的新 API,是写测试的前提
- Go further with App Intents — 深入 App Intents 高级用法,测试场景更丰富
- Design great widgets — Widget 也依赖 App Intents 提供数据,测试框架同样适用
- What’s new in Siri — Siri 体验的质量取决于 App Intents 的正确性
评论
GitHub Issues · utterances