电脑上代码已经写完,真正执行 iOS UI 测试时却卡在“没有 Mac”、模拟器无法启动,或者 SSH 命令结束后没有留下任何可分析结果。
最快解法:本周先采用“本地编码、远程 Mac 自动测试”的双环境架构,先跑通 1 个 Scheme 和 1 个 Simulator,再接入 Test Plan、xcresult 与定时任务;低频测试按需启动,每日回归或夜间任务再考虑常驻。
SECTION 01 谁适合把测试迁到远程 Mac?
这篇教程适合使用 Windows 或 Linux 编写跨平台代码、但必须运行 iOS 测试的独立开发者。
如果你是单人 App 作者,想把耗时的 XCTest 或 UI 测试从日常开发机迁出,或者你只需要一个可恢复的测试环境,而暂时不想搭建复杂 CI 平台,也可以按本文执行。
你不需要把源码编辑、调试和所有工作都搬到远程主机。更稳妥的分工是:
- 本地电脑:代码编辑、提交分支、快速静态检查和日常调试。
- 远程 Mac:恢复依赖、构建测试产物、启动 iOS Simulator、执行 XCTest 与 UI 测试。
- 结果查看端:下载或转存
.xcresult、控制台日志、截图和录屏,再根据失败证据定位问题。
这种拆分的价值在于,远程环境承担“可重复执行”的任务,而不是替代你的开发桌面。你需要先确认 4 件事:测试类型、触发频率、目标 iOS 版本,以及一次测试成功的判定标准。
SECTION 02 动手前先划清测试边界
没有本地 Mac,也可以执行 iOS 单元测试和 UI 测试,但远程主机必须具备匹配的 macOS、Xcode 和 Simulator Runtime,并且 UI 测试运行时需要可用的图形会话。
Apple 的文档说明,Xcode 可以针对模拟器或实体设备构建并运行 App,但 Simulator 并不等同于真实设备,部分硬件特性仍需要实体设备验证。Apple 关于模拟器与实体设备运行方式的说明也建议把模拟器用于常规覆盖,把真实设备用于设备特性确认。
先按下面的决策条件选择架构:
- 若每天只执行少量单元测试,且 UI 回归并不频繁,选择按需启动的远程 Mac,测试结束后保留结果包即可。
- 若每天需要自动执行回归,或希望在夜间无人值守运行,选择常驻测试机,并配置断线恢复、重启后接单和产物保留。
- 若需要多个 iOS 系统版本或多个设备类型,先串行完成单设备验收;只有在确认测试彼此独立、资源足够时,才增加并行目标。
- 若测试依赖摄像头、蓝牙、推送或其他真实硬件能力,不要把 Simulator 结果当成最终结论,应增加实体设备验收环节。
- 若项目仍无法通过一个共享 Scheme 构建,先修复项目基线,不要急着配置定时任务或并行执行。
远程 Mac 的兼容性不能只看“能否安装 Xcode”。你应按照 Xcode 系统要求与版本兼容矩阵核对 macOS、Xcode、SDK、Device Support 和 Simulator Runtime 的组合;不同版本之间并不是随意搭配。
⚠️ 注意:不要先购买或租用多个测试节点,再反过来迁移项目。先用一个目标设备、一个目标系统版本和一个 Scheme 完成最小链路,失败时你才能判断问题来自项目、工具链、模拟器还是远程会话。
SECTION 03 第一个小时:固定远程 Mac 的工具链
第一小时的目标不是跑完整回归,而是让远程主机具备可检查、可复现的测试基础。
1.确认系统与 Xcode 指向
连接远程 Mac 后,先记录以下信息:
sw_vers
xcodebuild -version
xcode-select -p
xcrun simctl list runtimes
xcrun simctl list devices
如果机器上存在多个 Xcode,使用 xcode-select 指向本次项目要求的开发目录。不要只在图形界面里切换版本,却让 SSH 会话继续使用另一个路径。
Simulator Runtime 是与具体平台和系统版本对应的操作系统包;如果目标 Runtime 没有安装,设备列表可能存在,但测试仍无法启动。Apple 关于添加 Simulator Runtime 的文档说明了 Runtime 与模拟器设备之间的关系。
2.建立独立目录与账号边界
建议把目录拆成 4 类:
~/ios-test/source/
~/ios-test/derived-data/
~/ios-test/results/
~/ios-test/logs/
项目名称、Scheme、Test Plan、用户名和仓库地址都使用你自己的值,不要把真实凭据写进脚本。测试账号与日常管理账号分开,SSH 只负责传代码、执行命令和取回产物;需要检查 Simulator 画面、权限弹窗或 UI 状态时,再通过 VNC 或网页控制台进入图形会话。
纯命令行测试可以由 SSH 触发,但涉及 Simulator 和 UI 框架的任务,不能只验证 SSH 登录是否成功。Apple 的远程命令行测试说明提到,远程执行涉及 UI 框架和 Simulator 时,需要正确创建 Aqua 图形会话。Apple 的远程命令行测试说明可作为排查依据。
因此,你应在测试服务商的交付说明中确认:登录会话是否可保持、重启后是否能恢复,以及图形会话中是否能够启动 Simulator。
3.先运行最小测试
选择一个不会修改外部数据的 XCTest,执行依赖恢复和最小构建。使用 Swift Package Manager 的项目可以先运行:
cd ~/ios-test/source/PROJECT_DIR
xcodebuild \
-scheme "SCHEME_NAME" \
-resolvePackageDependencies
如果项目是 .xcworkspace,应把 -project 改成 -workspace,并确认 Scheme 已共享。此时不要同时加入多个 destination,也不要直接运行完整 UI 回归。
你要观察的不是“终端有没有输出”,而是以下链路是否逐项成立:
- 源码目录可读,依赖能够恢复;
- 指定 Scheme 能解析;
- 目标 Simulator 出现在设备列表中;
- 测试进程能够启动并结束;
- shell 能获得明确的退出状态。
SECTION 04 首次执行:用 xcodebuild 跑通单一入口
在远程 Mac 上自动执行 XCTest 时,应从一个共享 Scheme 和一个明确的 destination 开始,再用退出状态判断任务是否真正成功。
最小入口可以写成:
set -o pipefail
xcodebuild test \
-workspace "WORKSPACE.xcworkspace" \
-scheme "SCHEME_NAME" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION' \
-resultBundlePath "$HOME/ios-test/results/first-run.xcresult" \
2>&1 | tee "$HOME/ios-test/logs/first-run.log"
status=${PIPESTATUS[0]}
exit "$status"
这里的 set -o pipefail 和退出状态保存很重要。否则即使 xcodebuild 失败,后面的 tee 仍可能让脚本看起来执行成功。
Apple 官方说明,xcodebuild test 可以通过 -destination 指定测试目标,并且测试失败时会返回非零退出状态;-only-testing 可以把范围缩小到单个测试目标、测试类或测试方法。Apple 的命令行构建与测试说明给出了这些参数的语义和格式。
当完整测试失败时,先用 only-testing 缩小范围:
xcodebuild test \
-workspace "WORKSPACE.xcworkspace" \
-scheme "SCHEME_NAME" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION' \
-only-testing:"TEST_TARGET/TEST_CLASS/TEST_METHOD" \
-resultBundlePath "$HOME/ios-test/results/single-test.xcresult"
你可以把测试分成 3 个 Test Plan,而不是给所有任务使用同一套入口:
FastUnitTests:提交前快速执行的 XCTest 和 Swift Testing。FullRegression:单元测试、集成测试和 UI 测试。ReleaseVerification:发布前需要更完整环境和诊断产物的测试。
Test Plan 可以定义测试范围、配置和执行方式,同一个 Scheme 也可以关联多个 Test Plan。Apple 的 Test Plan 组织指南还说明,较新的 Xcode 版本可以使用 Swift Testing 标签筛选测试。
命令中显式指定 Test Plan:
xcodebuild test \
-workspace "WORKSPACE.xcworkspace" \
-scheme "SCHEME_NAME" \
-testPlan "TEST_PLAN_NAME" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION'
不要在首次成功前加入过多参数。先让一个 Scheme、一个 Test Plan 和一个 Simulator 完整通过,再把测试拆分为快速任务、完整回归和发布前验证。
SECTION 05 首次 UI 测试:先固定模拟器状态
XCTest 失败时,不要马上把原因归结为远程 Mac 性能不足。至少要区分 4 类问题:
- 应用代码缺陷:同一测试在干净环境中稳定失败。
- 测试代码问题:等待条件、元素定位或异步处理不可靠。
- Simulator 状态问题:权限、登录状态、缓存和残留进程影响结果。
- 远程会话问题:图形会话消失、Simulator 未启动或重启后环境未恢复。
首次运行 UI 测试时固定以下条件:
- 设备类型与系统版本;
- 语言和地区;
- 测试账号与初始数据;
- 通知、定位、相册等权限;
- 测试前是否重置内容和设置;
- 测试后是否保留失败现场。
“清理”与“重置”不是一回事。删除 App 只能清除应用本身的部分状态;重置 Simulator 可能影响设备数据、权限和调试现场。遇到偶发失败时,先保存 .xcresult、控制台日志和截图,再执行破坏性清理。
🧪 经验:第一次验收不要把“启动 Simulator、安装 App、登录账号、执行 30 个 UI 测试、上传结果”写成一个长脚本。把每一步单独记录,才能知道失败发生在启动、安装、测试还是结果收集阶段。
SECTION 06 无人值守运行:把失败证据留下来
远程测试结果应通过 -resultBundlePath 写入独立的 .xcresult 目录,并同时保存完整控制台日志、截图、录屏和退出状态。
在 xcodebuild test 中显式指定结果路径:
RESULT="$HOME/ios-test/results/first-run.xcresult"
open "$RESULT"
xcrun xcresulttool get --format json --path "$RESULT" \
> "$HOME/ios-test/logs/first-run.json"
Apple 文档说明,测试命令生成的 Xcode Test Results 结果包可以包含测试会话结果、代码覆盖率(启用时)和其他日志,并能在 Xcode 中打开或共享。Apple 的测试结果查看文档对此有明确说明。
结果包不是“自动修复器”。它能告诉你测试失败的名称、部分日志、诊断信息和覆盖率,但未必能单独解释网络依赖、时序竞争或远程图形会话中断。因此,每次任务至少保留:
- 一个
.xcresult; - 一份完整控制台日志;
- 失败测试对应的截图或录屏;
- Git 提交标识与测试配置;
- 退出状态和失败时间。
Apple 的 Xcode 11 发布说明还记录了结果包可以通过 xcresulttool 检查,并可使用 xccov 查看覆盖率报告。Apple 关于结果包与 xcresulttool 的说明可用于设计结果分析脚本。
失败后的处理入口也应提前设计:
测试失败
→ 发送通知
→ 保留结果包与日志
→ 只重跑失败测试
→ 仍失败则保留 Simulator 状态供人工检查
→ 确认原因后再决定是否重置环境
不要默认“失败后立即删除所有缓存”。如果现场被清掉,后续只能重新猜测问题。
SECTION 07 第一周里程碑:从一次成功变成可恢复服务
第一周的目标不是追求最高并发,而是证明这个测试服务器在真实项目中可以重复工作。
先用测试重复功能识别偶发失败:
xcodebuild test \
-workspace "WORKSPACE.xcworkspace" \
-scheme "SCHEME_NAME" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION' \
-only-testing:"TEST_TARGET/TEST_CLASS/TEST_METHOD" \
-test-iterations 10 \
-run-tests-until-failure \
-resultBundlePath "$HOME/ios-test/results/repetition.xcresult"
重复测试适合发现状态污染、等待条件和时序问题,但不能证明测试一定稳定。Apple 的测试文档支持按最大次数重复、失败即停或成功即停等模式;具体参数应以当前安装版本的 xcodebuild -help 和官方文档为准。Apple 的测试重复说明提供了相关行为说明。
接着检查 5 个维护点:
- Simulator 是否在多次任务后留下残留进程。
DerivedData是否持续增长并挤占磁盘。.xcresult、截图和日志是否有保留期限。- SSH 断线后任务是否继续运行,结果是否仍能取回。
- Mac 重启后,登录会话、Simulator 和任务接收脚本能否恢复。
如果你需要把“构建测试产物”和“运行测试”拆开,可以采用:
xcodebuild build-for-testing \
-workspace "WORKSPACE.xcworkspace" \
-scheme "SCHEME_NAME" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION'
xcodebuild test-without-building \
-xctestrun "PATH_TO_XCTESTRUN" \
-destination 'platform=iOS Simulator,name=SIMULATOR_NAME,OS=IOS_VERSION'
这种拆分适合重复使用同一批测试产物,但不应忽略源码、依赖、Xcode 版本和目标设备的一致性。Apple 的测试工作流文档也将测试分为 build-for-testing 与 test-without-building 两个阶段。Apple 的测试工作流说明可用于核对拆分方式。
最后,用真实项目连续完成一次完整验收:
拉取指定提交
→ 恢复依赖
→ 执行 FastUnitTests
→ 执行 FullRegression
→ 导出 xcresult 与日志
→ 人为制造一次失败
→ 只重跑失败测试
→ 重启远程 Mac
→ 再次接收并完成测试任务
如果这条链路还不能稳定跑通,就先不要扩大设备数量或并行度。并行不是默认的加速按钮:共享测试数据、磁盘竞争、模拟器残留和图形会话资源都可能让总结果更难解释。
SECTION 08 远程 Mac 还是本地方案:按测试频率做选择
本地 Mac 的优点是图形调试直接、物理设备连接方便,适合每天频繁修改 UI 并即时查看结果;缺点是你的主力开发机需要长期承担 Simulator、缓存和回归任务,磁盘与使用时间也会被持续占用。
远程 Mac 更适合以下情况:
- Windows 或 Linux 是你的主要编码环境;
- 你需要一个持续在线的 iOS 测试节点;
- UI 回归和夜间测试不希望占用本地电脑;
- 你希望把测试日志、结果包和失败重跑集中保存。
但如果项目长期高负载运行、必须接入特定实体设备,或你每天都需要在 Simulator 画面中交互调试,租赁并不一定是最优长期方案。按需测试可以先选择短期远程 Mac;当每日回归、持续 XCTest 或夜间任务成为固定流程后,再比较常驻租期,并在开通前确认 Xcode、Simulator Runtime、图形会话和重启恢复能力。
你可以先查看 VPSNIX 的 Mac 远程使用说明,确认远程连接、SSH、图形会话和环境交付方式;若要进一步比较当前可用方案,再参考 VPSNIX 的套餐页面。如果测试节点经常需要重启后恢复连接,也可以结合 远程 Mac 重启后的离线恢复方法检查运维流程。
当你已经用一个 Scheme、一个 Simulator 和一个 Test Plan 跑通最小链路后,决策会清晰很多:偶尔回归,就按需启动;每天执行或需要无人值守,就使用常驻环境。相比把所有 XCTest 和 UI 测试塞进本地电脑,远程 Mac 能把持续运行、失败留档和重启恢复独立出来;相比直接搭建复杂 CI 平台,它又保留了真实 macOS 环境的控制权。若你需要临时测试节点、短期回归环境或持续运行的 iOS 自动化测试服务器,可以先用自己的项目验证远程 Mac,再决定适合的租赁周期。