测试代码积累到一定程度后,会出现一个新问题:“这些测试应该以什么组合运行?”
全部运行时,CI(Continuous Integration,持续集成)每次要花20分钟;只运行部分测试,又得手动反复切换选项。
想分别在韩语和英语环境中运行UI测试时,也可能找不到合适的方法。
先说结论,Apple 为解决这个问题提供的就是测试计划(Test Plan)。
Xcode 11 引入的测试计划会把“要运行的测试”和“运行条件”拆分到文件中管理。
本文将一次讲清测试计划的结构、实战配置和CI集成。
如果还不熟悉测试代码,可以先阅读使用 XCTest 基础和 Given-When-Then 编写第一个测试的方法。
什么是测试计划
测试计划是扩展名为.xctestplan的文件,内容是人类可读的JSON。添加到项目后,从Scheme中引用。
一个文件包含两部分内容。
- 要运行的测试:决定包含或排除哪些测试目标、套件和函数。Xcode 16 及更高版本还可以使用 Swift Testing 标签作为条件
- 配置(Configuration):决定以什么条件运行这些测试
关键在于它是文件。.xctestplan可以提交到Git,和团队共享相同条件,并让设置变更进入代码审查。
团队使用时,最好将计划和引用它的共享Scheme一起进行版本控制。
为什么仅靠Scheme设置不够
测试计划出现以前,所有运行条件都在Scheme的Test操作中。这种结构存在限制。
一个Scheme只有一套Test操作设置。因此要区分快速验证和夜间完整验证,就必须复制Scheme。
复制后,构建、运行和配置文件设置也会全部带过来。为了改一个测试条件,等于复制其余所有设置。
测试计划反转了这种关系。一个Scheme可以关联多个测试计划,每个计划又可以包含多个配置。
创建测试计划
根据Apple 当前的说明,Xcode会创建一个默认计划,包含Scheme构建的测试目标中的所有测试。
- 在Product > Scheme > Edit Test Plan中打开并保存默认计划
- 在Tests标签页按目标、标签、套件和函数选择运行对象
- 在Configurations标签页添加共享设置和所需配置
要创建更多计划,请使用Product > Test Plan > New Test Plan。旧Scheme的编辑器中可能会出现Convert to use Test Plans,这就是Xcode 11 引入的现有设置转换流程。
关联多个计划后,在Product > Test Plan > Manage Test Plans中将一个设为默认(Default)。未另行指定时就使用它。
在Product > Test Plan中启用要运行的计划。此时执行Command + U,也就是Product > Test,当前计划会按每个配置运行一次。应区分默认计划和当前启用的计划。
共享设置与配置的关系
打开测试计划编辑器会看到Tests和Configurations两个标签页,核心是Configurations。
此标签页分为上下两层。
- Shared Settings(共享设置):所有配置继承的默认值
- 单独配置:只覆盖共享设置中必要项目的变体
可以把它理解为CSS的继承:公共条件只写一处,各配置只指定差异项。
被覆盖的项目会在编辑器中加粗,一眼就能看出“只有这里不同”。
可设置的项目很多,常用的主要有这些。
| 项目 | 决定什么 |
|---|---|
| Arguments / Environment Variables | 运行参数和环境变量 |
| Application Language / Region | 应用运行时的语言和地区 |
| Code Coverage | 是否收集覆盖率以及覆盖哪些目标 |
| Execution Order | 按字母顺序运行,还是每次运行随机打乱 |
| Test Repetition Mode | 测试重复运行方式 |
| Test Timeouts | 单个测试允许的最长运行时间 |
| Runtime Sanitization | Address·Thread·Undefined Behavior Sanitizer |
| Memory Management | Malloc Scribble, Malloc Guard Edges, Zombie Objects |
| Automatic Screen Capture | 失败时是否自动附加屏幕截图 |
公共设置放在共享设置中,差异放在配置中。遵守这一原则,计划再多也不会失控。
拆分配置的实战示例
最常见的是按语言配置。多语言应用经常因语言不同导致布局错乱,拆分配置后就能自动按语言重复运行同一套UI测试代码。
- 共享设置:开启覆盖率,失败时保存截图
- 配置A“Korean”:将Application Language设为韩语
- 配置B“English”:将Application Language设为英语
- 配置C“RTL Pseudolanguage”:用于验证从右向左书写的语言
运行一次该计划后,所选测试在每个配置中运行一次,结果报告也会按配置分别显示。哪个语言失败会一目了然。
内存检查也是如此。根据Apple文档,Address Sanitizer可能使用2~3倍内存,并让代码变慢2~5倍,必须考虑运行成本。
平时的配置关闭sanitizer,另设诊断配置。例如分别建立Address Sanitizer和Thread Sanitizer配置,在夜间构建中运行。
覆盖率、随机顺序与重复运行
下面介绍配置中尤其有用的三个选项。
代码覆盖率不只是打开开关,更重要的是先确定测量对象。如果把依赖库混入同一个数值,就难以看清应用代码的变化。
选择“some targets”,只指定应用目标,才能得到有意义的数值。
在Execution Order中可以选择Alphabetical或Random。但不能笼统地说所有测试默认都是Alphabetical。
XCTest计划选择Random后,每次运行都会打乱顺序。另一方面,Swift Testing默认并行运行测试函数,顺序也会随机化。需要分别理解两个框架的行为。
如果改变顺序就失败,很可能依赖前一个测试留下的状态。这是发现测试间隐藏耦合的有用信号。这种独立性在优秀单元测试的FIRST原则中也很重要。
Test Repetition是Xcode 13 新增的选项,可以选择以下重复方式。
| 模式 | 行为 | 用途 |
|---|---|---|
| Up Until Maximum Repetitions | 不论结果如何,重复到指定的最大次数 | 测量间歇性失败的复现率 |
重复到失败(-run-tests-until-failure) |
一直重复到发生失败 | 追踪偶尔失败的测试 |
| Retry on Failure | 失败后重试到指定次数 | 保障不稳定UI测试在CI中的通过率 |
根据Xcode 13 发行说明,Maximum Test Repetitions必须指定正整数。官方文档未确认“默认3次”,因此直接检查计划中保存的值才准确。
命令行中使用-test-iterations设置次数,还可以组合-run-tests-until-failure或-retry-tests-on-failure。该命令行设置优先于计划中的重复设置。
Retry on Failure很方便,但应谨慎使用。靠重试通过会让不稳定测试继续存在。
可以临时开启,但最好另行调查不稳定的原因。
在CI中使用测试计划
测试计划也可以直接在命令行中使用。先用xcodebuild -scheme MyApp -showTestPlans确认Scheme关联的计划。-testPlan传入的是计划名称,而不是文件路径。
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-testPlan Smoke \
-destination 'platform=iOS Simulator,name=iPhone 16'
保持同一个Scheme,只切换计划名称,就能划分CI任务的运行范围。例如:
- 每个PR(Pull Request):
-testPlan Smoke— 以核心单元测试为主 - 合并到主分支:
-testPlan Regression— 全部单元测试和UI测试 - 夜间计划任务:
-testPlan Nightly— 包含sanitizer和多语言配置
还可以按配置进一步拆分。
例如Apple 当前的命令行示例,--only-test-configuration只运行指定配置;相反,--skip-test-configuration会排除指定配置后运行。两个选项都使用两个连字符。
如果计划有五个语言配置,CI系统可以创建五个任务,每个任务分配一个配置。但并行分配是CI系统的工作,不是测试计划自动分配机器或缩短运行时间的功能。
xcodebuild test \
-scheme MyApp \
-testPlan Localization \
--only-test-configuration Korean \
-destination 'platform=iOS Simulator,name=iPhone 16'
常见问题
Q. 测试计划文件要提交到Git吗?
A. 如果团队要共享相同的运行条件,建议提交。但上传到Git不是Xcode的必需条件。请同时管理计划及其引用的共享Scheme。
不过JSON在多人同时修改时会产生冲突。按计划目的拆分文件可以减少冲突。
Q. 创建多个配置后,时间不会翻倍吗?
A. 是的。测试会按配置数量重复运行。
因此,多语言和sanitizer等耗时配置适合从日常计划中移出,放到夜间计划运行。
Q. 一个计划中可以同时放单元测试和UI测试吗?
A. 可以。但单元测试按秒计,UI测试按分钟计,反馈速度差异很大。
拆成快速计划和慢速计划更符合开发流程。
Q. 用Swift Testing编写的测试也能放入计划吗?
A. 可以。选定的目标同时包含XCTest和Swift Testing测试时,可在同一计划中运行。了解Swift Testing的标签与并行运行方式也有助于拆分计划。
Q. 只想排除某些测试怎么办?
A. 在Tests标签页中,可以按目标、套件、函数或参数化的单个用例取消勾选。Xcode 16 及更高版本还可以使用 Swift Testing 的 Include Tags 和 Exclude Tags 选择范围。
它只记录在计划文件中,不修改代码,因此其他计划仍会正常运行。
测试计划不是用来重新编写测试的工具,而是决定已有测试运行哪些、使用什么条件的工具。
如果一直通过复制Scheme划分条件,请保存默认计划,再按目的整理计划。在CI中按任务明确指定计划和配置可以清晰界定范围,但实际节省时间取决于运行器数量和并行方式。
先在Product > Scheme > Edit Test Plan中保存默认计划,再创建一个Smoke计划。之后添加配置会容易得多。
来源与确认标准
- Improving code assessment by organizing tests into test plans (Apple) — 已确认当前Xcode的计划创建菜单、测试选择单位、按配置运行及
xcodebuild选项。 - Testing in Xcode, WWDC19 (Apple) — 已确认Xcode 11的引入时间、
.xctestplan文件结构和现有Scheme转换流程。 - Xcode 13 Release Notes (Apple) — 已确认重复模式、正整数重复次数和命令行选项优先级。
- Go further with Swift Testing, WWDC24 (Apple) — 已确认Swift Testing默认并行运行及随机运行顺序。
- Diagnosing memory, thread, and crash issues early (Apple) — 已确认sanitizer的作用和Address Sanitizer的运行成本。
确认基准日为2026年8月16日。菜单名称和命令行写法优先采用上述Apple官方文档当前的表述;对于文档未确认的默认重复次数或CI缩时数据,不作断言。

