iOS 应用把登录回调、活动页、订单详情和通知落点交给深链后,一次路由重构就可能让链接停在首页,或者在应用已打开时重复压入同一页面。人工点击几个链接只能证明当时可用,无法覆盖冷启动、前台状态、编码参数和非法输入。更稳妥的做法,是在云端 Mac 上固定模拟器环境,把每条深链的最终路由变成持续集成可读取的结果。
先把链接写成可审查的契约
不要从测试脚本里零散拼 URL。先维护一份路由清单,明确输入、期望路由和应用状态。
| 场景 | 输入 | 期望结果 |
|---|---|---|
| 商品详情 | linklab://product/42 |
product/42 |
| 搜索参数 | linklab://search?q=swift%20ui |
search?query=swift ui |
| 缺少标识 | linklab://product/ |
error/invalid-product |
| 未知路径 | linklab://unknown |
error/not-found |
清单中的期望值应是路由器解析后的标准形式,而不是某个按钮标题。这样界面文案变化不会制造误报,测试也能直接指出路径、参数还是状态恢复出了问题。
深链门禁验证的是“输入是否到达正确业务路由”,不是“系统是否接受了一条 URL”。命令返回成功不等于页面正确。
同一路由至少准备冷启动和前台两种案例。涉及鉴权时,再区分已登录与未登录,但测试账户状态必须由脚本显式建立,不能继承上一条用例。
给调试构建加入可观测探针
命令行能够打开链接,却无法可靠判断应用最终展示了什么。可在 DEBUG 构建中,让统一路由入口把标准化结果写入缓存目录。发布构建不包含这段逻辑,也不要记录令牌或完整查询参数。
#if DEBUG
func recordResolvedRoute(_ route: String) {
let payload: NSDictionary = [
"route": route,
"recordedAt": Date().timeIntervalSince1970
]
let directory = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
)[0]
let file = directory.appendingPathComponent("link-probe.plist")
payload.write(to: file, atomically: true)
}
#endif
探针应放在 URL Scheme、Universal Link 和通知跳转共同经过的路由器之后。若分别在生命周期回调中记录,测试可能只证明回调被触发,却没有证明参数经过校验并成功落到业务页面。
保持结果原子化
每次执行前删除旧文件,写入时使用原子替换。否则应用崩溃或读取时机过早,可能让本次测试误读上一次结果。建议只记录路由键、错误类型和时间戳,用户输入先脱敏。
固定模拟器并执行路由矩阵
CI 不要使用含糊的 booted 作为起点。先按设备名称和系统版本解析 UDID,确认只有目标模拟器启动,再等待系统完成引导。下面假设流水线已经得到 SIMULATOR_UDID。
set -euo pipefail
xcrun simctl shutdown all
xcrun simctl boot "$SIMULATOR_UDID"
xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcodebuild \
-scheme LinkLab \
-configuration Debug \
-sdk iphonesimulator \
-derivedDataPath .ci/DerivedData \
build
APP_PATH=".ci/DerivedData/Build/Products/Debug-iphonesimulator/LinkLab.app"
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
安装后先取得数据容器,再逐条清理探针、打开链接并轮询结果。不要写固定的长时间 sleep;应用启动速度变化时,它要么浪费时间,要么仍然读不到文件。
BUNDLE_ID="com.example.linklab"
DATA_DIR="$(xcrun simctl get_app_container \
"$SIMULATOR_UDID" "$BUNDLE_ID" data)"
PROBE="$DATA_DIR/Library/Caches/link-probe.plist"
rm -f "$PROBE"
xcrun simctl openurl "$SIMULATOR_UDID" "linklab://product/42"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
test -f "$PROBE" && break
sleep 1
done
test -f "$PROBE"
ACTUAL="$(/usr/libexec/PlistBuddy -c "Print :route" "$PROBE")"
test "$ACTUAL" = "product/42"
前台案例在首次打开后继续发送 URL;冷启动案例则先执行 simctl terminate。每条失败用例保存输入、期望值、实际值和测试日志,但不要归档包含秘密信息的完整应用容器。
Universal Link 要分两层验收
URL Scheme 适合稳定地验证路由器,但不能替代 Universal Link 的端到端检查。后者还依赖关联域权限、HTTPS 站点文件、内容类型、应用标识和系统缓存。
第一层在每次提交中直接调用统一路由器,快速覆盖路径标准化、百分号编码、重复参数、空值和未知路由。第二层在受控测试域执行真实 HTTPS 链接,重新安装应用后验证冷启动与前台切换。站点关联文件变更时必须触发第二层,不能只跑单元测试。
常见误区是修改关联文件后立即重复点击,并把旧缓存结果当成新配置。排查时应记录应用构建号、系统版本、安装顺序和测试域响应头;先证明系统把链接交给应用,再检查业务路由。
把失败证据和清理动作做进门禁
可靠门禁不仅要返回非零状态,还要让维护者快速判断故障层级。失败摘要至少包含用例名、应用状态、输入类型、期望路由、实际路由,以及探针文件是否生成。若文件不存在,优先检查安装、生命周期入口和链接声明;若文件存在但路由错误,再检查解析器与导航状态。
合并前清单可以保持简短:
- 模拟器型号与系统版本由流水线固定;
- 冷启动和前台状态分别执行;
- 每条用例先清理应用进程或探针状态;
- 特殊字符、空参数和未知路径都有反例;
- Universal Link 有独立的 HTTPS 验收;
- 失败产物不包含凭据、令牌和用户数据;
- 任务结束后关闭模拟器并删除临时构建目录。
最后执行 xcrun simctl shutdown "$SIMULATOR_UDID",并让工作区清理策略处理 .ci/DerivedData。当路由契约、可观测探针和状态隔离同时存在时,深链测试才从“链接能打开”升级为可阻止导航回归的工程门禁。
常见问题
只用 simctl openurl 能证明 Universal Link 配置正确吗?
不能。它适合验证路由解析和应用行为,但 Universal Link 还依赖关联域声明、站点文件、TLS 与系统缓存,必须另设真实 HTTPS 验收。
深链测试为什么要分别覆盖冷启动和前台状态?
两种状态通常进入不同的生命周期回调。只测前台可能漏掉首次启动路由,只测冷启动则可能漏掉重复跳转和状态栈污染。
为构建、测试与实验选择独享物理节点
两档 M4 配置覆盖新加坡、东京、首尔与香港节点。实际可用状态以下单时返回的结果为准。