一条“记录阅读”的快捷指令在开发机上能找到,换到远程构建的模拟器却不出现。此时先别改业务代码:参数是否合法、Intent 能否执行、系统是否发现动作,是三项不同的检查。云端 Mac 适合固定前两项的测试环境;最后一项仍要在装有应用的模拟器里验收。
先划清自动测试的边界
本文用一个不写入磁盘的示例动作验证流程:输入书籍编号,去掉首尾空白,返回规范化编号。这样能先排除存储权限和网络波动。将来接入数据库时,再单独测试写入及重复执行策略,不要把“返回成功”误当成“数据已保存”。
验收分三层:纯 Swift 规则检查空值和非法字符;XCTest 调用 perform() 检查返回值与错误;人工在模拟器中确认动作可见、参数可填写、结果可读取。前两层失败应阻断合并。第三层受系统索引影响,适合作为版本发布前的检查项,而不是用固定等待秒数编写测试。
不要在自动测试里不断卸载、重装应用,只为等快捷指令出现。先证明代码契约正确,再排查应用安装、启动和系统发现过程。
把输入规则做成可直接测试的类型
下面的规则接受 1–32 位 ASCII 字母、数字、下划线和连字符。空格只允许出现在输入首尾;其他字符返回明确错误。示例中的 perform() 只返回编号,没有持久化副作用。
import AppIntents
import Foundation
enum ReadingIDError: LocalizedError {
case invalid
var errorDescription: String? {
"编号须为 1–32 位字母、数字、下划线或连字符"
}
}
enum ReadingID {
static func normalize(_ raw: String) throws -> String {
let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let pattern = #"^[A-Za-z0-9_-]{1,32}$"#
guard value.range(of: pattern, options: .regularExpression) != nil else {
throw ReadingIDError.invalid
}
return value
}
}
struct CheckReadingIDIntent: AppIntent {
static var title: LocalizedStringResource = "检查书籍编号"
static var openAppWhenRun = false
@Parameter(title: "书籍编号")
var bookID: String
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let normalized = try ReadingID.normalize(bookID)
return .result(value: normalized)
}
}
把参数标题、动作标题和错误提示纳入工程的本地化资源。这里的中文是示例文案,不应在多语言应用里直接作为全部语言的交付文本。规则本身只接受 ASCII 编号;若业务允许汉字或其他文字,应先定义允许的字符集,再修改表达式和测试样本。
覆盖边界,而不只测正常输入
至少检查 " book_7 " 返回 "book_7",空字符串、内部空格及超过 32 位的编号被拒绝。错误分支应断言错误类型,不要只断言“抛出了某种错误”:否则底层依赖意外抛错也可能让测试通过。测试 perform() 时,再确认其返回值与规范化规则一致。将纯规则测试与 Intent 调用测试分开,失败时才能判断是输入规则变了,还是系统接口的包装层变了。
注册入口并在模拟器中检查
需要让系统展示一个建议动作时,可在应用目标中加入 AppShortcutsProvider。短语保留应用名占位,让系统把动作关联到当前应用;它不是 shell 字符串。
import AppIntents
struct ReadingShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CheckReadingIDIntent(),
phrases: ["在 \(.applicationName) 检查书籍编号"],
shortTitle: "检查书籍编号",
systemImageName: "book"
)
}
}
先确认这份文件属于应用目标,而非仅属于测试目标。安装构建后的应用并启动一次,再打开模拟器中的“快捷指令”,搜索动作标题,填写一个有效编号执行。随后提交非法编号,检查错误是否可读。若动作未出现,记录系统版本、应用构建版本及安装后的操作顺序;不要把“重新打开页面后碰巧出现”写成确定的修复方案。
固定测试设备与结果目录
在 ZSHMac 的云端 Mac 上,先用 xcrun simctl list devices available 查看已安装且可用的模拟器,选定设备 UDID,设置 SIM_UDID。下面的命令以工程含有名为 Reader 的共享 scheme 为前提;实际执行时使用自己工程的 scheme。每次运行都给结果包一个新路径,避免已有文件导致测试命令直接失败。
mkdir -p artifacts
test -n "${SIM_UDID:-}" || { printf '%s\n' 'SIM_UDID is required' >&2; exit 1; }
result="$PWD/artifacts/app-intents-$(date +%Y%m%d-%H%M%S)-$$.xcresult"
xcodebuild test \
-scheme Reader \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath "$result"
在 CI 中保留命令退出码及结果包:退出码决定任务是否通过,结果包供失败后定位测试用例。若 scheme 未共享、目标模拟器不可用,先修运行入口,不要把它归类为 Intent 逻辑失败。手动验收则记录动作是否出现、输入提示是否正确,以及有效和无效参数的实际表现。
失败时按层排查
| 现象 | 首先检查 | 不要先做 |
|---|---|---|
| 纯规则测试失败 | 字符范围、长度及首尾空白处理 | 重装模拟器 |
perform() 结果不符 |
参数传入与返回值类型 | 等待系统索引 |
| 测试命令找不到设备 | 可用设备列表与 UDID | 修改 Swift 规则 |
| 快捷指令没有动作 | 应用目标成员关系、安装和首次启动 | 给测试加固定延时 |
| 动作存在但执行报错 | 参数提示、错误类型及目标系统版本 | 把所有错误吞掉 |
这份分层检查的价值是缩小故障范围,而不是保证系统入口在安装后立即可见。代码测试通过、模拟器动作也完成有效与无效输入检查,才算这次验收闭环。接入真实写入后,还要另加重复执行和数据落盘的测试,不能沿用本例“只返回编号”的通过标准。
常见问题
XCTest 通过后,为什么快捷指令里仍找不到动作?
先确认包含 AppShortcutsProvider 的应用已安装并启动,再检查测试设备的系统版本与应用目标配置。系统发现入口可能有延迟;不要把动作立即出现作为单元测试断言。
参数校验应该放在 App Intent 里还是业务层?
把可复用的格式规则放进独立的 Swift 类型,App Intent 的 perform() 调用它。这样单元测试不依赖系统索引,其他入口也能沿用同一规则。
无界面 CI 能完全替代快捷指令手动验收吗?
不能。CI 适合固定参数、错误和返回值契约;系统是否展示动作、是否正确提示输入参数,还需在目标系统版本的模拟器或设备上检查。
需要一台独享的云端 Mac?
选择机型、节点和计费周期。交付的是独享物理机,实际可用性以控制台实时返回为准。