症状:GitHub Actions 的 UI 测试越来越慢,matrix 展开后却仍然排队,失败还难以复现。
最快解法:先用 Xcode Test Plans 或 only-testing 按稳定依赖边界拆片,再把每个分片路由到多台独立远程 Mac;只有 1 台 Runner 时,矩阵任务不会形成真正的跨节点并发。
这套方法适合测试套件已经超过单次可接受反馈窗口的团队。若你的测试仍依赖共享登录、固定账户或连续业务状态,应先治理依赖,再谈并行。
谁该看这篇?
- iOS 测试工程师:需要把不断增长的 XCTest UI 套件拆成可独立执行、可单独复现的分片。
- DevOps 工程师:需要配置 GitHub Actions matrix、Runner 标签、并发控制和结果汇总。
- 研发负责人:需要根据队列、稳定性和节点利用情况,判断是否增加远程 Mac。
先建立基线:你要优化的是哪一段时间?
分片前不要先改 YAML。先让一轮完整 UI 测试留下可比较的基线,至少记录以下内容:
- 构建和安装耗时。
- 真正的测试执行耗时。
- GitHub Actions Job 排队耗时。
- 每个测试类或测试套件的耗时分布。
- 首次失败的位置、截图和控制台日志。
- 最终生成的
.xcresult结果包。
这一步的价值在于区分 3 种并发。GitHub Actions Job 并发决定有多少任务能被调度;Xcode Simulator 并行决定单台 Mac 上可以启动多少测试进程或 Simulator 克隆;Swift Testing 进程内并行则属于测试框架内部行为,不能把它当成 Runner 数量。
GitHub 官方文档说明,每个 Runner 一次只能运行 1 个 Job;矩阵只是生成多个 Job,实际能否同时运行还取决于可用 Runner。GitHub Actions 的矩阵最多可生成 256 个 Job,但这不代表你拥有 256 个测试执行槽位。(GitHub Actions 关于工作原理的官方文档)
怎么判断瓶颈?
- 排队时间明显,Runner 长时间处于忙碌状态:优先检查节点数量和路由标签。
- Job 已经同时启动,但 Simulator 变慢、安装失败或出现超时:优先检查单节点资源和隔离。
- 每次运行失败的测试位置不稳定,且失败集中在共享账户或连续流程:优先修复测试状态,不要扩容。
- 构建耗时占大头:比较每个分片重复构建与
build-for-testing后复用测试产物。
Apple 的测试结果文档建议通过 .xcresult 查看测试会话、日志、截图以及可选的代码覆盖率。不要只看 GitHub Job 的总耗时,否则很容易把构建、排队和测试执行混成一个指标。(Apple 关于运行测试与解读结果的文档)
第一次分片:按依赖边界拆,不按文件数量平均
XCTest 分片的第一原则不是“每片放一半文件”,而是“每片能够独立准备、执行和失败复现”。
Xcode Test Plans 可以为同一个 Scheme 组织不同测试集合和配置。你可以为开发反馈、Pull Request 检查和发布前全量验证分别建立 Test Plan,再通过测试 Target、Suite、测试函数或标签缩小执行范围。(Apple 关于组织测试以改善反馈的文档)
如何用 Test Plan 建立可独立运行的测试组?
在 Xcode 中为目标 Scheme 创建多个 Test Plan,并明确每个计划包含哪些 Target、Suite 和测试函数。进入命令行后,可以用 -testPlan 选择计划,也可以用 -only-testing 进一步选择某个 Target、Suite 或测试函数。Apple 文档给出的测试标识格式包括 test_target/test_suite 和 test_target/test_type/test_function。(Apple 关于运行测试与解读结果的文档)
建议先设计类似下面的分片清单:
| 分片标识 | 测试范围 | 必须固定的输入 | 独立复现入口 |
|---|---|---|---|
ui-auth |
登录、登出、会话失效 | 测试账户、后端环境 | Test Plan 或 only-testing |
ui-core-flow |
核心业务流程 | 初始数据、推送状态 | 指定 Suite |
ui-settings |
设置、权限和本地状态 | Simulator 权限、清理策略 | 指定测试 Target |
ui-regression |
发布前关键回归 | 固定设备和账户 | 串行基线任务 |
登录状态、共享账户、推送环境和连续业务流程不要强行拆开。例如,测试 A 创建订单、测试 B 读取订单,如果 B 依赖 A 的副作用,就不能只因为两个文件大小接近而把它们放入不同分片。
每个分片还要写清 4 项信息:固定标识、输入数据、预计职责、单独复现命令。然后用一次全量测试核对两件事:
- 每个测试只出现 1 次。
- 没有测试因为 Test Plan 排除规则而静默消失。
⚠️ 注意:Test Plan 中被排除的测试不会以“失败”显示。它可能只是没有被执行,所以分片验收必须同时检查测试清单,而不能只看通过数量。(Apple 关于组织测试以改善反馈的文档)
第一次接入矩阵:让 Job 找到正确的远程 Mac
完成分片后,再把分片映射到 GitHub Actions matrix。示例中的仓库、Scheme、设备、标签和路径全部使用占位符:
jobs:
ios-ui:
strategy:
fail-fast: false
max-parallel: 2
matrix:
shard:
- ui-auth
- ui-core-flow
- ui-settings
runs-on:
group: <RUNNER_GROUP>
labels: [self-hosted, macos, <XCODE_LABEL>, <SIMULATOR_LABEL>]
steps:
- uses: actions/checkout@<CHECKOUT_VERSION>
- name: Prepare isolated paths
run: |
echo "SHARD=${{ matrix.shard }}" >> "$GITHUB_ENV"
mkdir -p "$RUNNER_TEMP/${{ matrix.shard }}/results"
mkdir -p "$RUNNER_TEMP/${{ matrix.shard }}/derived-data"
- name: Run selected UI tests
run: |
xcodebuild test \
-scheme "<SCHEME>" \
-testPlan "<TEST_PLAN>" \
-only-testing:"<TEST_TARGET>/<TEST_SUITE>" \
-derivedDataPath "$RUNNER_TEMP/${SHARD}/derived-data" \
-resultBundlePath "$RUNNER_TEMP/${SHARD}/results/${SHARD}.xcresult"
- name: Upload shard evidence
if: always()
uses: actions/upload-artifact@<UPLOAD_VERSION>
with:
name: xcresult-${{ matrix.shard }}
path: ${{ runner.temp }}/${{ matrix.shard }}/results
max-parallel 只是矩阵任务的并发上限,不会创建新的 Runner。GitHub 默认会根据 Runner 可用情况尽量并行;如果只有 1 台符合标签和 Group 条件的空闲 Runner,多个分片仍会排队。(GitHub Actions 工作流语法官方文档)
GitHub 路由使用标签和 Runner Group。组合条件会同时生效:Runner 必须属于指定 Group,并且匹配所有指定标签。你可以把 Xcode 主版本、处理器架构、Simulator 设备类型或项目权限抽象为标签,但标签不能替代节点健康检查。(GitHub 自托管 Runner 标签官方文档)
矩阵任务怎样才能真正同时执行?
正确路径是:把每个可独立运行的套件放入 matrix,使用 runs-on 将 Job 路由到符合条件的自托管 Mac,再用 max-parallel 控制同时放出的 Job 数量。矩阵数量、max-parallel 和实际 Runner 数量必须同时满足,否则你得到的只是更多排队任务。
还要区分 concurrency。它通常用于取消同一分支上过时的工作流,或限制同类工作流同时运行;它不是 Simulator 并行开关。配置前先确认你希望取消旧任务,还是保留所有 Pull Request 的测试证据。
重复构建,还是复用测试产物?
有两条可选路径:
- 每个分片独立构建:配置简单,失败边界清楚,但会重复消耗构建时间和磁盘。
- 先
build-for-testing,再分发测试产物:可以减少重复构建,但需要可靠地上传、下载并校验.xctestrun、应用和测试 Bundle,流程更复杂。
第一次接入建议优先选择可复现性更高的路径。等基线确认构建确实是主要瓶颈,再引入产物复用。所有命令参数都应在目标节点执行 xcodebuild -help 再确认,不要把另一台 Xcode 的参数直接复制过来。
第一次并发:先隔离 Simulator 和临时状态
单机并行和多节点分片不是同一件事。
Apple 早期 Xcode 文档说明,iOS 和 tvOS Simulator 的并行测试可以通过多个测试 Runner 进程和 Simulator 克隆执行,命令行中的并行 Worker 参数可以覆盖界面设置。具体默认行为与资源占用会随 Xcode 版本、项目和测试内容变化,因此不要用固定数字推断容量。(Apple Xcode 10 发布说明)
单个 Mac Runner 的 Simulator 并发量该怎么判断?
从 GitHub Actions 角度看,1 台 Runner 一次执行 1 个 Job。这个 Job 内部能否运行多个 Simulator 测试进程,则由 Xcode 测试配置、Worker 参数、CPU、内存、磁盘和测试本身决定,没有一个适用于所有项目的固定答案。(GitHub Actions 关于工作原理的官方文档)
第一次试跑时,至少隔离以下路径和资源:
- 每个分片独立的
DerivedData。 - 每个分片独立的
.xcresult路径。 - 独立的
$RUNNER_TEMP子目录。 - 不重复占用的端口。
- 不共享会被修改的测试账户。
- 不依赖上一个分片残留的 Simulator 状态。
- 不把截图、日志和附件写入同一个固定文件名。
每次只调整一个并发层级。先运行单节点串行,再运行单节点内部并行,最后运行多节点 matrix。观察系统资源、Simulator 日志、失败截图和测试顺序。如果并发一提高就出现安装失败、启动超时或大量状态污染,先回退并发,不要马上增加远程 Mac。
决策条件可以这样执行:
- 若排队时间占总反馈时间的主要部分,且节点处于忙碌状态:增加独立远程 Mac 或调整 Runner 路由。
- 若排队很短,但 Simulator 启动和测试执行明显变慢:降低单节点内部并行,保留多节点分片。
- 若失败集中在共享账户或连续流程:合并相关分片,修复测试隔离。
- 若同一提交在串行通过、并发失败:保留串行基线,检查资源与共享状态。
- 若分片耗时差距很大:按测试耗时和依赖重新组合,而不是继续增加矩阵数量。
- 若发布前结果必须高度稳定:保留串行或双轨复测入口。
结果汇总:把失败分成基础设施、测试和数据问题
每个分片都应独立上传:
.xcresult。- 控制台日志。
- 失败截图或视频。
- 分片标识和执行清单。
- Scheme、Test Plan、设备和提交信息。
多个测试分片的 xcresult 不能简单拼接成一个“总通过率”。汇总 Job 应先检查矩阵是否完整,再检查每个结果包是否存在,最后判断失败类型。
怎样保存和检查多个分片的测试结果?
先按分片名称保存每个结果包,并在汇总阶段建立清单。缺少结果包属于基础设施失败;结果包存在但断言失败,才进入测试失败分析。你可以在 Xcode Report navigator 中查看失败测试、日志、附件和覆盖率,也可以使用 Apple 文档列出的命令行结果包路径进行后续处理。(Apple 关于运行测试与解读结果的文档)
重试只能用于识别波动,不能用第二次成功覆盖第一次失败。建议把结果分为:
✅ 真实断言失败:测试逻辑或产品行为失败。
⚠️ 不稳定测试:同一提交在相同条件下结果不一致。
❌ 基础设施失败:Runner 离线、Simulator 无法启动、磁盘或权限异常。
⚠️ 数据失败:账户、服务端数据或外部依赖不满足。
覆盖率也不能把多个百分比直接相加。只有在工具确认覆盖数据可以安全合并,并且各分片使用兼容的构建与符号信息时,才可以输出合并结果;否则应分别展示分片覆盖率,并标明统计范围。
上线验收:用真实 Pull Request 决定并发模式
首次上线不要只跑一轮示例任务。连续观察真实 Pull Request,至少比较:
- 端到端反馈时间。
- Runner 排队时间。
- 最长分片耗时。
- 分片之间的耗时差距。
- 失败复现率。
- 单节点资源压力。
- 远程 Mac 的空闲与忙碌状态。
- 丢失结果包和基础设施失败次数。
如果最长分片长期主导总耗时,说明拆分仍不均衡,应继续按耗时和依赖重组。若排队时间主导总耗时,才有理由评估增加远程 Mac。继续扩大 matrix 而不增加可用节点,只会制造更长的队列。
| 运行方式 | GitHub Job 并发 | Simulator 并行 | 适合场景 | 主要风险 |
|---|---|---|---|---|
| 单节点串行 | 低 | 关闭或低 | 发布前基线、失败复现 | 反馈慢 |
| 单节点内部并行 | 1 个 Job | 由 Xcode 配置决定 | 套件已消除共享状态 | 资源争用、Simulator 启动失败 |
| 多节点 matrix | 取决于空闲 Runner | 每节点独立配置 | 可拆分的 UI 套件 | 节点路由和结果汇总复杂 |
| 双轨运行 | 关键任务串行,全量任务并行 | 分别控制 | 发布前稳定性与日常反馈兼顾 | 维护两套入口 |
| 观察结果 | 应选方案 | 回滚入口 |
|---|---|---|
| 分片独立、失败稳定、Runner 经常排队 | 增加独立远程 Mac | 将 matrix 改回串行基线 |
| 节点充足,但单节点内部并行导致资源异常 | 多节点低并发 | 关闭 Xcode 内部并行 |
| 测试依赖共享账户或连续状态 | 合并相关分片并修复隔离 | 保留原始全量 Test Plan |
| 发布前要求结果一致 | 双轨:日常分片,发布串行 | 直接执行串行 Test Plan |
| 构建明显重复且结果可复现 | 评估 build-for-testing 产物复用 |
回退到分片独立构建 |
如果你还没有稳定的 Mac 节点,先完成 Mac mini M4 远程租赁方案 的 Xcode、Simulator、磁盘和权限验收,再把它加入 Runner Group。需要比较不同地域交付时,可以查看 KVMNODE 的远程 Mac 节点选择,但不要在验收前仅凭节点数量判断测试容量。
单台本地 Mac 或单节点 Runner 的优点是配置简单、失败定位直接;缺点是所有 UI 测试共享同一排队入口,机器维护会占用研发时间,节点离线时整个流水线都会等待。云端 Linux 主机则无法直接提供 Xcode、Simulator 和完整 macOS 工具链。
因此,当前方案如果主要问题是排队和 macOS 节点不足,租赁独立的远程 Mac 通常比继续堆 matrix 更合理;如果你的测试长期高负载、需要固定物理接口或必须完全控制硬件,直接自购 Mac 仍可能更适合。建议你先完成单节点基线和两分片试跑,确认等待来自 Runner 不足,而不是共享状态或 Simulator 故障,再通过 KVMNODE 评估短期测试节点。