Highlight
Xcode Cloud 支持在多平台、多系统版本上并行运行测试套件,但测试量扩大后不可靠测试会拖垮 CI 流程。Apple 通过
XCTSkip、XCTExpectFailure、测试重复执行、并行测试和超时控制等机制,帮助开发者写出既可靠又快速的测试。
核心内容
测试环境不可靠?从 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 - 不再需要
XCTestExpectation和wait(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
- Meet Xcode Cloud — Xcode Cloud 入门,了解 Workflow 和 Start Condition 配置
- Write tests to fail — 如何编写高质量的测试用例
- Get your test results faster — 进一步优化测试执行速度
- Embrace Expected Failures in XCTest — 深入理解 XCTExpectFailure 的使用场景
评论
GitHub Issues · utterances