WWDC Quick Look 💓 By SwiftGGTeam
Author fast and reliable tests for Xcode Cloud

Author fast and reliable tests for Xcode Cloud

观看原视频

Highlight

Xcode Cloud 支持在多平台、多系统版本上并行运行测试套件,但测试量扩大后不可靠测试会拖垮 CI 流程。Apple 通过 XCTSkipXCTExpectFailure、测试重复执行、并行测试和超时控制等机制,帮助开发者写出既可靠又快速的测试。

核心内容

测试环境不可靠?从 setup 开始

你在本地跑测试一切正常,但一上 CI 就失败。问题出在 Xcode Cloud 使用的是全新的模拟器,时区、Locale、网络权限、预置数据都可能和本地不一样。

Apple 的建议是:把所有状态准备放在 setUp() 里,不要在 tearDown() 里为下一个测试做清理。每个测试独立负责自己的初始状态。

03:37

条件不满足时,跳过测试

某些测试只在特定环境运行。比如生产环境的下单接口不能真的调用,否则会产生真实订单。XCTSkip 可以在运行时检查条件并跳过测试。

06:00

已知失败的测试,不要静音

外部服务维护期间,相关测试会失败。直接禁用测试会丢失覆盖率,用 XCTExpectFailure 可以把失败标记为”预期失败”,测试套件整体仍算通过,同时保留对问题的追踪。

10:02

并行测试加速 CI

Xcode Cloud 默认并行测试多平台。你还可以在 test plan 中开启”Execute in parallel”,让多个 test target 和 test class 在服务器多核上并发执行。

16:37

防止失控测试拖垮整个套件

某个测试陷入死循环或等待超时,会让整个 CI 流程卡住。在 test plan 的 Configurations 中开启”Test Timeouts”,设置单测试最大执行时间(默认 600 秒),超时会自动标记失败并继续执行后续测试。

17:52

详细内容

用 async setUp 准备测试数据

var truck: Truck!

override func setUp() async throws {
    let directoryURL = FileManager.default.temporaryDirectory
    let fileName = UUID().uuidString
    let fileURL = directoryURL.appendingPathComponent(fileName, isDirectory: false)
    let data = await mockDonutMenuData()
    try data.write(to: fileURL)
    truck = Truck(menuURL: fileURL)
}

关键点:

  • setUp() 标记为 async throws,支持异步数据准备和错误抛出
  • 使用 temporaryDirectory 避免污染真实文件系统
  • UUID().uuidString 确保每个测试使用独立的文件名,避免并行测试冲突
  • setUp() 中完成所有状态初始化,不依赖 tearDown() 清理

用环境变量控制测试流程

var truck: Truck!

func testOrderDonut() throws {
    let host = ProcessInfo.processInfo.environment["BASE_URL"]
    try XCTSkipIf(host == "prod.example.com")

    let expectation = XCTestExpectation(description: "Order donut")
    truck.order(with: .sprinkles, host: host) { error, donut in
        XCTAssertTrue(donut.hasSprinkles)
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 5)
}

关键点:

  • ProcessInfo.processInfo.environment 读取环境变量
  • XCTSkipIf 在条件满足时跳过测试,标记为 skipped
  • Xcode Cloud 中环境变量需加 TEST_RUNNER_ 前缀,如 TEST_RUNNER_BASE_URL,运行时前缀会被自动剥离
  • Test Plan 中配置环境变量时不需要加前缀

用 async/await 替代 expectation 超时

func testOrderDonut() async throws {
    let host = ProcessInfo.processInfo.environment["BASE_URL"]
    try XCTSkipIf(host == "prod.example.com")

    let donut = try await truck.orderDonut(with: .sprinkles, host: host)
    XCTAssertTrue(donut.hasSprinkles)
}

关键点:

  • 测试方法标记为 async throws,直接调用异步 API
  • 不再需要 XCTestExpectationwait(for:timeout:)
  • 没有超时限制,测试会等待异步操作完成
  • 代码更简洁,避免了因服务器慢导致的超时失败

标记预期失败

func testOrderDonut() async throws {
    let host = ProcessInfo.processInfo.environment["BASE_URL"]
    try XCTSkipIf(host == "prod.example.com")

    XCTExpectFailure("https://dev.myco.com/bug/98 Donut ordering service is down")
    let donut = try await truck.orderDonut(with: .sprinkles, host: host)
    XCTAssertTrue(donut.hasSprinkles)
}

关键点:

  • XCTExpectFailure 接收一个描述字符串,通常放 bug 链接
  • 被包裹的断言失败会被记录为”expected failure”
  • 测试套件整体仍算通过,避免已知问题干扰 CI 结果
  • 服务恢复后,测试会自动恢复为正常通过状态

配置 Pull Request 专用测试计划

// 在 Test Plan 的 Tests 标签页中选择子集
// 例如:只选单元测试 + 关键 UI 测试

关键点:

  • 创建专门的”Pull Requests” test plan,只包含核心测试
  • 在 Xcode Cloud Workflow 中设置 Start Condition 为”Pull Request Changes”
  • 添加 Build action 和 Test action,选择 Pull Requests test plan
  • 另建一个定时 Workflow 运行完整测试套件

核心启发

  • 为不同环境创建分层测试计划:核心测试在 PR 时快速验证,完整测试定时在后台运行。入口:Xcode -> Product -> Test Plan -> 新建,Xcode Cloud -> Manage Workflows。

  • 用环境变量控制测试行为:在 Xcode Cloud Workflow 或 Test Plan 中配置 TEST_RUNNER_* 环境变量,测试代码中通过 ProcessInfo 读取,实现一套测试代码适配开发/测试/生产多环境。

  • 把异步测试迁移到 async/await:将基于 XCTestExpectation 的回调式测试改写为 async throws 测试方法,消除超时参数,减少因网络波动导致的 flaky test。

  • 为已知问题使用 XCTExpectFailure:不要禁用或跳过因外部依赖失败的测试,用 XCTExpectFailure 包裹,既保留测试覆盖率,又在问题修复后自动恢复。

  • 设置单测试超时防止 CI 卡死:在 Test Plan -> Configurations -> Test Timeouts 中设置合理的执行时间上限,防止单个失控测试拖垮整个 CI 流程。

关联 Session

评论

GitHub Issues · utterances