本地运行通过,单独运行也通过,但远程 CI 重复执行时随机失败,通常说明测试之间存在未被声明的共享依赖,或节点环境正在放大资源竞争。
本周建议动作:先固定提交、Xcode、测试入口和节点状态,隔离共享状态与外部资源;只有暂时无法重构的范围才加 .serialized,再用远程 Mac 的重复运行和干净工作区复测是否可以恢复并行。不要先全局关闭并行。
谁应该看这篇
如果你正在把 XCTest 测试迁移到 Swift Testing,并且测试顺序改变后出现随机失败,这篇文章适合你。
如果你负责远程 Mac 测试流水线、结果采集、节点资源隔离,或者需要决定“重构、局部串行化还是增加独立节点”,下面的角色边界可以直接作为排查分工。
截至 2026 年 9 月 6 日,本文已核对 Apple Developer Documentation 和 swift-testing 官方仓库。Swift Testing 默认并行运行测试,并提供 .serialized 对测试函数、参数化测试和 Suite 做有限串行化;它也可以与 XCTest 共存。Apple 的 Swift Testing 并行执行文档 明确了这些行为。
先区分 4 种并发,否则修错层
“CI 并行导致测试失败”这句话太粗。你至少要拆成下面 4 层:
| 并发层级 | 典型执行方式 | 主要风险 | 第一责任人 |
|---|---|---|---|
| Swift Testing 进程内并行 | 同一测试进程中的测试或参数案例并行 | 全局变量、单例、静态缓存、共享对象互相污染 | 测试作者 |
| XCTest 并行 | XCTest 测试类被多个 Runner 进程分配 | 类级状态、共享文件和模拟器资源冲突 | 应用工程师 |
| CI Job 并发 | 多个流水线同时使用测试节点 | 端口、DerivedData、临时目录和 Keychain 争用 | CI 工程师 |
| 多个 Runner 并发 | 多台 Runner 或多个执行器共享环境 | 节点残留、后台任务、磁盘和内存压力 | 平台负责人 |
Swift Testing 默认在进程内并行,参数化测试的不同案例也默认并行。官方还特别说明,Suite 中的测试默认彼此并行,因此不能只看测试文件结构来判断实际执行关系。Swift Testing 总览、参数化测试文档
第二个容易误判的点是:.serialized 只控制 Swift Testing 自己能控制的范围。它不会自动把整个 CI Job、XCTest Runner、模拟器进程或其他流水线变成串行。
失败证据先于修复动作
测试作者与 CI 工程师先建立同一份记录
你需要把一次“随机失败”拆成可比较的样本,而不是只截一张 CI 红灯截图。每次运行至少保留:
- Git 提交哈希;
- Xcode 和 Swift 工具链版本;
- Scheme、Test Plan 和命令行入口;
- 失败测试、参数值和执行顺序;
.xcresult结果包;- 节点内存压力、磁盘空间和后台任务;
- 是否使用了缓存、复用 DerivedData 或复用模拟器。
使用 xcodebuild 执行测试时,Xcode 会生成 .xcresult 结果包,其中包含会话结果、日志以及可选的代码覆盖率信息。Apple 的测试结果文档
用 3 组运行把问题分类
先不要修改代码。按下面顺序执行:
- 用固定提交和固定测试入口连续运行并行测试。
- 把失败用例单独运行,观察是否仍然失败。
- 使用干净克隆、独立 DerivedData、独立临时目录再次运行。
- 将失败用例放回原测试集合,比较单独运行与集合运行的差异。
- 重启远程 Mac 后重复一次,排除残留进程、模拟器和 Keychain 状态。
结果可以先按这 4 类标记:
| 观察结果 | 更可能的方向 | 不要急着做的事 | 下一步 |
|---|---|---|---|
| 单独运行也失败 | 稳定代码缺陷或环境缺陷 | 不要加 .serialized 掩盖失败 |
检查输入、工具链和节点日志 |
| 单独通过,集合中失败 | 顺序依赖或共享状态 | 不要先删除全部缓存 | 检查全局状态、夹具和清理逻辑 |
| 只有并行运行失败 | 资源竞争或非安全并发 | 不要直接判定 Swift Testing 有缺陷 | 逐项隔离文件、端口和系统资源 |
| 干净节点通过,复用工作区失败 | 残留环境或节点卫生问题 | 不要修改业务测试断言 | 清理工作区并调整节点隔离 |
这一步的价值在于,你可以把“框架行为”和“项目没有隔离好”分开。官方设计文档本身就将默认并行视为发现隐藏依赖的一种方式,并建议只对确实需要的测试范围退出并行。Swift Testing 设计愿景
04测试作者:先清除共享状态和顺序依赖
迁移 XCTest 时,最常见的变化不是断言语法,而是测试生命周期和实例假设发生变化。Swift Testing 的 Suite 不要求继承 XCTestCase;实例测试通常由测试系统创建 Suite 实例后再调用测试函数。不要继续假设所有测试共享同一个类实例。从 XCTest 迁移到 Swift Testing
重点检查这些位置:
static或全局可变变量;- 单例中的缓存、令牌和当前用户;
- 静态数据库连接或共享内存对象;
- 测试之间复用的数组、字典和临时模型;
setUp、tearDown中没有对等清理的逻辑;- 异步任务启动后,测试函数已经结束;
- 通知监听器、定时器或后台 Task 没有取消。
优先改成“每个测试独立创建夹具”。如果测试需要数据库,使用独立数据库文件或独立 schema;如果测试需要配置对象,避免从全局单例读取可变值。异步测试必须等待任务完成,并在结束时取消监听器和后台任务。
完成重构后,不要只跑一次。用随机顺序、重复执行和集合执行验证。若顺序变化后仍然通过,才说明测试隔离有了初步证据。
05应用工程师:文件、端口与系统资源必须有命名空间
应用层资源冲突经常被误认为是 Swift Testing 并行模型的问题。实际上,两个测试即使业务逻辑完全不同,只要写入同一个外部资源,也可能互相覆盖。
常见冲突包括:
- 固定名称的临时目录;
- 固定路径的 SQLite 或 JSON 文件;
- 写死的本地网络端口;
- 共用
UserDefaultssuite; - 共用 Keychain service 和 account;
- 没有移除的通知监听器;
- 同一个模拟器中的文件、账户和推送状态;
- UI 测试与进程内单元测试同时操作同一应用状态。
建议给每个测试生成唯一命名空间,并在 defer 或等价清理路径中删除。端口不要写死,可以让系统分配可用端口,再把端口传给被测对象。Keychain 测试则应使用专用 service 标识,并在成功、失败和取消路径都执行清理。
模拟器和 UI 任务要单独判断。XCTest 的模拟器并行通常涉及多个 Runner 进程和模拟器副本,而 Swift Testing 的进程内并行是另一条执行路径。你不能用一个“关闭并行”开关解释两种冲突,也不能因为单元测试稳定,就推断 UI 测试资源已经隔离。
如果你还没有稳定的测试节点,可以先阅读 Xcode 测试节点工作区隔离方法,把源码目录、DerivedData、临时目录和测试产物分开,再回到测试代码定位问题。
06.serialized 的范围要小,位置要对
Apple 当前文档给出的规则可以直接转成下面的选择:
| 写法 | 适合的场景 | 实际影响 | 需要注意 |
|---|---|---|---|
@Test(.serialized, arguments: values) |
参数案例共享同一资源 | 该参数化测试的案例串行执行 | 只影响这个测试 |
@Suite(.serialized) |
Suite 内多项测试共享状态 | Suite 内测试和子 Suite 递归串行 | 无关 Suite 仍可并行 |
非参数化函数加 .serialized |
想保护单个普通测试 | 官方说明下通常没有效果 | 不能用它表达普通测试间的全局依赖 |
全局 --no-parallel |
临时确认并发相关性 | 整个测试入口关闭并行 | 只能作为诊断或回退手段 |
对参数化测试,.serialized 应该放在测试函数上。对一组共同依赖数据库、固定文件或进程状态的测试,放在 Suite 上更合理。官方明确指出,Suite 级 .serialized 会递归作用于其中的测试和子 Suite,但不会阻止无关测试与该 Suite 并行。Apple 的 .serialized 语义说明
示例:
import Testing
@Suite(.serialized)
struct DatabaseIntegrationTests {
@Test(arguments: ["create", "update", "delete"])
func databaseOperation(_ operation: String) async throws {
// 共享同一个测试数据库,暂时按顺序执行
}
}
如果只有一个测试函数依赖全局状态,可以使用更窄的范围。对于暂时无法描述清楚的进程级共享状态,官方源码还提供了带依赖范围的实验性写法,但不要在生产流水线中未经验证地引入实验性 API。swift-testing 官方实现
什么时候不应该加 .serialized
有 3 种情况不适合直接串行化:
- 失败其实是稳定断言错误;
- 失败只发生在复用工作区,清理后消失;
- 资源可以通过唯一目录、独立端口或独立 Keychain 标识解决。
串行化可以帮助你确认“是否存在并发相关性”,但不能证明测试已经正确。每个 .serialized 都要记录原因、依赖资源、临时性和移除条件。否则几个月后,团队只会记得“这个 Suite 很脆弱”,却没人知道它为什么不能并行。
迁移负责人:把 Swift Testing 与 XCTest 分开验收
Swift Testing 和 XCTest 可以在同一个测试目标、同一个测试包中共存。Apple 也建议渐进式迁移,而不是一次性重写全部测试。Xcode 测试目标组织说明
迁移期间要建立一张清单:
- 哪些测试已经使用
Testing模块; - 哪些测试仍然继承
XCTestCase; - 哪些目标包含 UI 测试或性能测试;
- 当前 Scheme 使用哪一份 Test Plan;
- CI 是否显式指定了测试计划;
- 本地和 CI 是否使用相同的
-only-testing范围; - 是否混用了两套框架的断言和生命周期假设。
XCTest 的 UI 测试和性能测试仍有各自的执行语义。不要看到同一个测试包里同时存在两种框架,就把所有并行失败归咎于“混用”。先按测试框架、测试目标和 Runner 进程拆分记录。
Test Plan 也不能只看文件名。它决定哪些测试运行,以及测试操作使用哪些配置。一个 Scheme 可以关联多份 Test Plan,默认计划和命令行显式指定的计划可能不同。Apple 的 Test Plan 文档
08CI 工程师:远程 Mac 节点要先证明自己干净
远程 Mac 上的随机失败,除了代码问题,还可能来自节点压力和工作区复用。你需要检查:
- 多个 Job 是否落到同一台 Mac;
- 是否复用同一 DerivedData;
- 是否多个进程写同一个临时目录;
- 测试期间是否有构建、索引或其他后台任务;
- 磁盘空间是否持续下降;
- 模拟器是否残留上一次运行的数据;
- Keychain、通知监听器和本地服务是否跨 Job 存活。
建议每个 Job 使用独立工作目录。源码可以从干净克隆开始,DerivedData 使用 Job 唯一路径,临时目录携带流水线编号,测试产物单独归档。失败后不要立即清理全部证据,先保存 .xcresult、系统日志、测试命令、进程列表和节点状态。
你可以把复测分成 4 个阶段:
- 复用当前工作区运行并行测试。
- 只替换为独立 DerivedData。
- 再替换为干净克隆和独立临时目录。
- 重启远程 Mac 后重复同一提交。
如果只有第一阶段失败,重点查工作区卫生。如果 4 个阶段都只在并行时失败,重点回到测试资源隔离。如果重启后仍然失败,节点残留的可能性下降,但不能因此直接判定是框架缺陷。
09平台负责人:用证据决定重构、串行还是拆节点
最终决策不应该是“所有红灯都关闭并行”。可以按下面的条件执行:
✅ 继续并行:失败无法在并行模式稳定复现;干净环境、重启后和重复运行均通过;测试没有发现共享资源。
✅ 局部使用 .serialized:失败只集中在明确依赖同一资源的 Suite 或参数化测试;资源暂时无法重构;已经记录移除条件。
✅ 重构测试隔离:失败来自固定目录、端口、Keychain、通知监听器、全局缓存或异步任务未完成;修改后可以用独立命名空间消除冲突。
✅ 增加独立远程 Mac 节点:不同 Job 之间互相污染,且清理成本高于节点隔离成本;或者 UI、模拟器、签名和集成测试需要不同的系统状态。
⚠️ 全局关闭并行:只能作为短期回退或诊断手段。它会隐藏测试之间的依赖,可能让 CI 看起来稳定,却没有修复根因。
验收至少要包含:
- 干净工作区运行;
- 同一提交重复运行;
- 并行与局部串行对照;
- 远程 Mac 重启后复测;
- 失败结果包和节点日志可追溯;
.serialized有责任人和移除条件;- CI Job 与 Runner 的资源边界明确。
如果你需要持续运行多个 Xcode 测试任务,可以进一步参考 远程 Mac 构建节点并发与容量估算,先把 Job 并发和节点独占关系画清楚,再决定是否扩容。
10当前电脑不适合复现时,远程 Mac 的价值在哪里
本地电脑和单独运行都通过,但 CI 随机失败时,最大的困难不是“有没有一台更快的机器”,而是缺少一个可重复、可丢弃、能保留完整证据的测试节点。
如果继续在当前电脑上复用工作区,你会面对固定缓存、残留模拟器、后台任务和本地资源状态;如果把任务放进普通 Linux 云服务器,又无法复现完整的 macOS、Xcode、模拟器和 Keychain 行为。自购 Mac mini 则需要承担硬件闲置、远程接入、系统维护和多人共享隔离成本。
更合适的做法是租用一台独立远程 Mac,用同一提交分别运行并行、局部 .serialized 和干净工作区方案。这样你能先判断该修测试、修节点,还是拆分流水线,而不是在一台长期被其他任务污染的电脑上反复猜测。若你只需要临时复现、迁移验收或建立短期 CI 测试节点,可以查看 VpsMesh 的 Mac 远程租赁方案;如果是长期稳定重负载并且需要物理接口,自购 Mac 仍然可能更合适。
这类问题的正确顺序很明确:先让失败证据可重复,再隔离共享资源;能重构就恢复并行,不能重构才局部串行,只有节点边界确实不清晰时才增加独立远程 Mac。