Highlight
Xcode 13 为 XCTest 加入测试重复执行(Test Repetition),开发者可以用固定次数、直到失败、失败重试三种模式复现不稳定测试,并用 Xcode、测试计划和 xcodebuild 收集诊断数据。
核心内容
测试偶发失败最难处理。
同一个测试,今天在本机通过,明天在 CI 失败。再点一次,又通过。问题可能来自竞态条件、环境假设、全局状态,或者外部服务通信。开发者没有稳定复现路径,只能看一次失败日志,猜下一步该查哪里。
Xcode 13 的测试重复执行解决的是这个入口问题。它让一个测试按指定次数反复运行,并给每次运行加上停止条件。失败不再只出现一次。你可以把它压出来,停在失败现场,再进调试器看对象状态。
Session 的例子是一个 IceCreamTruckCountdown App。测试 testFlavors 从 truckDepot 获取冰淇淋车,调用 prepareFlavors,最后断言车上有 33 种口味。这个测试在 Xcode Cloud 的主分支上偶发失败。开启失败重试后,报告显示前几次失败,最后一次通过。这说明问题会重复出现,只是概率不高。
开发者接着在本地对同一个测试运行 100 次。测试失败了 4 次。再把停止条件改成直到失败,并开启失败暂停,Xcode 在失败处停住。调试器里看到 flavors 还是 0,原因是 XCTestExpectation 在外层 completion handler 中完成,内层 prepareFlavors 还没结束。
修复方式是把测试改成 Swift 5.5 的 async/await。异步流程变成顺序代码,prepareFlavors 完成后才进入断言。重复运行 100 次后全部通过,开发者再移除测试计划里的失败重试配置。
详细内容
测试重复执行的三种模式
(00:56)Xcode 13 的测试重复执行可以设置最大迭代次数和停止条件。Session 介绍了三种模式:固定次数(Fixed iterations)、直到失败(Until failure)、失败重试(Retry on failure)。
# 固定次数:运行指定次数,结果不会提前停止
xcodebuild test -test-iterations 100
# 失败重试:失败后重试,直到成功或达到最大次数
xcodebuild test -test-iterations 10 -retry-tests-on-failure
# 直到失败:持续运行,遇到第一次失败就停止
xcodebuild test -test-iterations 100 -run-tests-until-failure
关键点:
xcodebuild test运行测试动作。-test-iterations 100指定最多运行 100 次。-retry-tests-on-failure对失败测试进行重试,用来在 CI 中临时收集不稳定测试的数据。-run-tests-until-failure遇到失败就停止,适合把非确定性失败带进调试器。
(01:10)固定次数适合观察测试套件的可靠性。它会运行完整次数,无论中间是否失败。团队可以用它检查新测试加入后,套件是否开始出现偶发失败。
xcodebuild test -test-iterations 100
关键点:
- 这个命令只设置迭代次数。
- 没有额外停止条件,所以每次迭代都会执行。
- 输出结果可以用来统计失败率,例如 100 次中失败 4 次。
(01:28)直到失败适合本地复现。它会持续运行测试,直到第一次失败。配合 Xcode 的 Pause on Failure,可以在失败现场停下。
xcodebuild test -test-iterations 100 -run-tests-until-failure
关键点:
-test-iterations 100给重复执行设置上限。-run-tests-until-failure把停止条件改为失败即停。- 本地调试时,开发者可以先用这个模式把偶发失败转成可检查的失败现场。
(01:41)失败重试适合临时诊断 CI。它会在测试失败后重试,直到测试成功或达到最大次数。Session 明确提醒,重试失败可能隐藏产品中的真实问题,所以应该在诊断完成后移除。
xcodebuild test -test-iterations 10 -retry-tests-on-failure
关键点:
-retry-tests-on-failure只在失败后重试。- 最终成功不代表第一次运行没有问题。
- 这个模式适合收集失败数据,不适合长期掩盖失败。
一个由 completion handler 引起的竞态条件
(02:39)testFlavors 的原始版本使用 XCTestExpectation 等待异步回调。代码看起来等待了冰淇淋车准备口味,实际等待点放错了位置。
func testFlavors() {
var truck: IceCreamTruck?
let flavorsExpectation = XCTestExpectation(description: "Get ice cream truck's flavors")
truckDepot.iceCreamTruck { newTruck in
truck = newTruck
newTruck.prepareFlavors { error in
XCTAssertNil(error)
}
flavorsExpectation.fulfill()
}
wait(for: [flavorsExpectation], timeout: 5)
XCTAssertEqual(truck?.flavors, 33)
}
关键点:
var truck: IceCreamTruck?先把车声明为可选值,因为它会在异步回调里赋值。XCTestExpectation用来让测试等待异步操作。truckDepot.iceCreamTruck { newTruck in ... }异步取回冰淇淋车。newTruck.prepareFlavors { error in ... }继续异步准备口味。flavorsExpectation.fulfill()放在外层回调里,早于内层prepareFlavors完成。wait(for:timeout:)只等待 expectation 完成,没有等待口味准备完成。XCTAssertEqual(truck?.flavors, 33)可能在flavors仍为 0 时执行。
(05:30)Xcode 在失败处暂停后,调试器显示 flavors 是 0。继续检查断点后,发现内层 completion handler 还没执行。根因在等待位置:测试过早结束等待,断言提前运行。
newTruck.prepareFlavors { error in
XCTAssertNil(error)
}
flavorsExpectation.fulfill()
关键点:
prepareFlavors的 completion handler 代表口味准备完成。fulfill()在 completion handler 外部执行。- 测试认为异步流程完成时,真正的业务操作还在运行。
- 这类错误在嵌套 completion handler 中很容易被忽略。
用 async/await 重写测试
(06:18)XCTest 支持 Swift 5.5 的 async/await。测试方法可以声明为 async throws,然后直接等待异步 API 完成。
func testFlavors() async throws {
let truck = await truckDepot.iceCreamTruck()
try await truck.prepareFlavors()
XCTAssertEqual(truck.flavors, 33)
}
关键点:
async throws让测试方法可以调用异步 API,并把错误交给 XCTest 处理。await truckDepot.iceCreamTruck()等待冰淇淋车返回。let truck不再是可选值,因为代码会等到返回后才继续执行。try await truck.prepareFlavors()等待口味准备完成,并处理可能抛出的错误。XCTAssertEqual(truck.flavors, 33)在两个异步步骤完成后执行。
(06:31)Session 展示了改写过程的第一步:先给测试方法加上 async throws。这一步只改变方法签名,为后续 await 调用打开入口。
func testFlavors() async throws {
var truck: IceCreamTruck?
let flavorsExpectation = XCTestExpectation(description: "Get ice cream truck's flavors")
truckDepot.iceCreamTruck { newTruck in
truck = newTruck
newTruck.prepareFlavors { error in
XCTAssertNil(error)
}
flavorsExpectation.fulfill()
}
wait(for: [flavorsExpectation], timeout: 5)
XCTAssertEqual(truck?.flavors, 33)
}
关键点:
async允许测试体内使用await。throws允许测试体内使用try await。- 此时测试逻辑还没修好,只是为改写异步调用做准备。
- 后续步骤会移除 expectation 和 completion handler。
(06:40)接着用异步版本的 iceCreamTruck,再用异步版本的 prepareFlavors。最终代码保留原来的断言目标,但去掉了手动等待对象。
func testFlavors() async throws {
let truck = await truckDepot.iceCreamTruck()
try await truck.prepareFlavors()
XCTAssertEqual(truck.flavors, 33)
}
关键点:
- 第一行函数签名说明这是异步测试。
- 第二行等待
truckDepot返回IceCreamTruck。 - 第三行等待车完成口味准备。
- 第四行检查口味数量是否等于 33。
- 代码顺序和业务顺序一致,错误的
fulfill()位置消失了。
在 Xcode 和 Xcode Cloud 中看迭代结果
(03:00)Session 先在测试计划中临时启用 Retry on failure,然后到 Report Navigator 查看 Xcode Cloud 报告。报告中的 iteration label 会标出每次运行的序号。开发者展开记录后,看到断言失败相同,最后一次通过。
xcodebuild test -test-iterations 10 -retry-tests-on-failure
关键点:
- 这个命令对应测试计划里的失败重试思路。
- CI 报告会保留多次迭代的结果。
- 多次失败信息相同,说明可以沿着同一个断言继续查。
- 最后一次通过只说明重试成功,不能说明问题已经消失。
(04:03)在本地,开发者可以 Control-click 测试钻石图标,选择 Run "testFlavors()" Repeatedly…。对话框里可以设置停止条件、最大重复次数,以及 Pause on Failure。
xcodebuild test -test-iterations 100 -run-tests-until-failure
关键点:
- Xcode 的图形界面适合临时复现单个测试。
xcodebuild适合在脚本和 CI 中覆盖测试计划设置。- Session 的例子把最大次数设为 100,本地复现出 4 次失败。
- 第二次运行选择失败即停后,Xcode 自动勾选 Pause on Failure,便于进入调试器。
核心启发
-
做什么:给 CI 增加一个“重复执行单个失败测试”的诊断任务。 为什么值得做:Session 展示了用失败重试收集 Xcode Cloud 中的不稳定测试数据。 怎么开始:在 CI 脚本里对目标测试追加
-test-iterations,按场景选择-retry-tests-on-failure或-run-tests-until-failure。 -
做什么:把偶发失败测试的失败率记录成趋势。 为什么值得做:固定次数模式可以衡量测试套件可靠性,新增测试后更容易发现稳定性下降。 怎么开始:定期运行
xcodebuild test -test-iterations 100,记录通过次数、失败次数和失败断言。 -
做什么:审计项目中嵌套 completion handler 的 XCTest。 为什么值得做:Session 的 bug 来自 expectation 在错误的 completion handler 中完成。 怎么开始:搜索
XCTestExpectation、wait(for:timeout:)和多层回调,确认fulfill()是否放在真正完成业务操作的位置。 -
做什么:把异步 XCTest 迁移到 async/await。 为什么值得做:XCTest 支持
async throws测试方法,异步步骤可以按业务顺序写出来。 怎么开始:先把测试方法改成async throws,再把可用的异步 API 替换为await或try await调用。 -
做什么:为本地调试建立“失败即停”习惯。 为什么值得做:直到失败模式可以把低概率失败带进调试器,减少只看日志的猜测。 怎么开始:在 Xcode 中 Control-click 测试钻石图标,选择
Run Repeatedly…,把停止条件设为 Until failure,并启用 Pause on Failure。
关联 Session
- Embrace Expected Failures in XCTest — 用
XCTExpectFailure标记已知问题,减少测试报告噪音。 - Meet async/await in Swift — 理解
async、await和从 completion handler 迁移的基本方式。 - Explore Xcode Cloud workflows — 在 Xcode Cloud 中配置构建、测试和分发流程。
- Explore Digital Crown, Trackpad, and iPad pointer automation — 用 Xcode 13 的新输入自动化能力扩展 UI 测试覆盖面。
- Discover breakpoint improvements — 用断点和 LLDB 在运行时检查失败现场。
评论
GitHub Issues · utterances