本地运行通过,单独运行也通过,但远程 CI 重复执行时随机失败,通常说明测试之间存在未被声明的共享依赖,或节点环境正在放大资源竞争。

本周建议动作:先固定提交、Xcode、测试入口和节点状态,隔离共享状态与外部资源;只有暂时无法重构的范围才加 .serialized,再用远程 Mac 的重复运行和干净工作区复测是否可以恢复并行。不要先全局关闭并行。

01

谁应该看这篇

如果你正在把 XCTest 测试迁移到 Swift Testing,并且测试顺序改变后出现随机失败,这篇文章适合你。

如果你负责远程 Mac 测试流水线、结果采集、节点资源隔离,或者需要决定“重构、局部串行化还是增加独立节点”,下面的角色边界可以直接作为排查分工。

截至 2026 年 9 月 6 日,本文已核对 Apple Developer Documentation 和 swift-testing 官方仓库。Swift Testing 默认并行运行测试,并提供 .serialized 对测试函数、参数化测试和 Suite 做有限串行化;它也可以与 XCTest 共存。Apple 的 Swift Testing 并行执行文档 明确了这些行为。

02

先区分 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、模拟器进程或其他流水线变成串行。

03

失败证据先于修复动作

测试作者与 CI 工程师先建立同一份记录

你需要把一次“随机失败”拆成可比较的样本,而不是只截一张 CI 红灯截图。每次运行至少保留:

  • Git 提交哈希;
  • Xcode 和 Swift 工具链版本;
  • Scheme、Test Plan 和命令行入口;
  • 失败测试、参数值和执行顺序;
  • .xcresult 结果包;
  • 节点内存压力、磁盘空间和后台任务;
  • 是否使用了缓存、复用 DerivedData 或复用模拟器。

使用 xcodebuild 执行测试时,Xcode 会生成 .xcresult 结果包,其中包含会话结果、日志以及可选的代码覆盖率信息。Apple 的测试结果文档

用 3 组运行把问题分类

先不要修改代码。按下面顺序执行:

  1. 用固定提交和固定测试入口连续运行并行测试。
  2. 把失败用例单独运行,观察是否仍然失败。
  3. 使用干净克隆、独立 DerivedData、独立临时目录再次运行。
  4. 将失败用例放回原测试集合,比较单独运行与集合运行的差异。
  5. 重启远程 Mac 后重复一次,排除残留进程、模拟器和 Keychain 状态。

结果可以先按这 4 类标记:

观察结果 更可能的方向 不要急着做的事 下一步
单独运行也失败 稳定代码缺陷或环境缺陷 不要加 .serialized 掩盖失败 检查输入、工具链和节点日志
单独通过,集合中失败 顺序依赖或共享状态 不要先删除全部缓存 检查全局状态、夹具和清理逻辑
只有并行运行失败 资源竞争或非安全并发 不要直接判定 Swift Testing 有缺陷 逐项隔离文件、端口和系统资源
干净节点通过,复用工作区失败 残留环境或节点卫生问题 不要修改业务测试断言 清理工作区并调整节点隔离

这一步的价值在于,你可以把“框架行为”和“项目没有隔离好”分开。官方设计文档本身就将默认并行视为发现隐藏依赖的一种方式,并建议只对确实需要的测试范围退出并行。Swift Testing 设计愿景

04

测试作者:先清除共享状态和顺序依赖

迁移 XCTest 时,最常见的变化不是断言语法,而是测试生命周期和实例假设发生变化。Swift Testing 的 Suite 不要求继承 XCTestCase;实例测试通常由测试系统创建 Suite 实例后再调用测试函数。不要继续假设所有测试共享同一个类实例。从 XCTest 迁移到 Swift Testing

重点检查这些位置:

  • static 或全局可变变量;
  • 单例中的缓存、令牌和当前用户;
  • 静态数据库连接或共享内存对象;
  • 测试之间复用的数组、字典和临时模型;
  • setUptearDown 中没有对等清理的逻辑;
  • 异步任务启动后,测试函数已经结束;
  • 通知监听器、定时器或后台 Task 没有取消。

优先改成“每个测试独立创建夹具”。如果测试需要数据库,使用独立数据库文件或独立 schema;如果测试需要配置对象,避免从全局单例读取可变值。异步测试必须等待任务完成,并在结束时取消监听器和后台任务。

完成重构后,不要只跑一次。用随机顺序、重复执行和集合执行验证。若顺序变化后仍然通过,才说明测试隔离有了初步证据。

05

应用工程师:文件、端口与系统资源必须有命名空间

应用层资源冲突经常被误认为是 Swift Testing 并行模型的问题。实际上,两个测试即使业务逻辑完全不同,只要写入同一个外部资源,也可能互相覆盖。

常见冲突包括:

  • 固定名称的临时目录;
  • 固定路径的 SQLite 或 JSON 文件;
  • 写死的本地网络端口;
  • 共用 UserDefaults suite;
  • 共用 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 很脆弱”,却没人知道它为什么不能并行。

07

迁移负责人:把 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 文档

08

CI 工程师:远程 Mac 节点要先证明自己干净

远程 Mac 上的随机失败,除了代码问题,还可能来自节点压力和工作区复用。你需要检查:

  • 多个 Job 是否落到同一台 Mac;
  • 是否复用同一 DerivedData;
  • 是否多个进程写同一个临时目录;
  • 测试期间是否有构建、索引或其他后台任务;
  • 磁盘空间是否持续下降;
  • 模拟器是否残留上一次运行的数据;
  • Keychain、通知监听器和本地服务是否跨 Job 存活。

建议每个 Job 使用独立工作目录。源码可以从干净克隆开始,DerivedData 使用 Job 唯一路径,临时目录携带流水线编号,测试产物单独归档。失败后不要立即清理全部证据,先保存 .xcresult、系统日志、测试命令、进程列表和节点状态。

你可以把复测分成 4 个阶段:

  1. 复用当前工作区运行并行测试。
  2. 只替换为独立 DerivedData。
  3. 再替换为干净克隆和独立临时目录。
  4. 重启远程 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。