Highlight
Apple 在 XCTest 中提供 XCTExpectFailure,让已知测试失败继续执行并在报告中标记为预期失败,从而减少噪声并保留新问题信号。
核心内容
一个测试昨天还在通过,今天开始失败。第一次失败很有价值。它可能说明产品有 bug,也可能说明测试写错了,还可能是依赖框架或系统行为发生了变化。
问题在于,团队不一定能马上修掉它。接下来的每次 CI 运行都会继续报同一个失败。红色结果还在,但它已经不再提供新信息。真正的新失败会被埋在旧失败里。
以前常见的处理方式有两个。第一种是在 test plan 或 scheme 里禁用测试。代码仍然会编译,但测试不执行,报告里也看不到这个问题。第二种是用 XCTSkip。测试会执行到跳过点,报告里有记录,但跳过点之后的代码不会运行,后续信息也丢了。
Apple 在这场 session 里介绍的工具是 XCTExpectFailure。它适合处理已经登记、暂时无法修复的失败。测试照常运行,命中的失败会在报告中变成 expected failure(预期失败),包含它的 test suite 会显示为通过的混合状态。你仍然能看到这个问题,也能更快发现新的失败。
已知失败仍然需要链接到缺陷
XCTExpectFailure 接受一个 failure reason(失败原因)。演讲建议把 bug 跟踪系统的 URL 放进这个字符串。Xcode 的测试报告会识别 URL,悬停时显示跳转按钮,方便从测试报告回到问题记录。
作用域越小,噪声越少
XCTExpectFailure 有两种用法。stateful(有状态)用法会影响调用之后的失败。scoped(作用域)用法把可能失败的代码包进闭包,闭包外的失败照常报告。共享测试工具函数里更适合用 scoped 写法,因为它减少对外层测试状态的影响。
严格模式帮你删除过期标记
默认行为是 strict(严格)。如果你声明了预期失败,但这次没有任何失败,XCTest 会生成一个新的失败,叫 unmatched expected failure。这会提醒你:底层问题可能已经修复,应该删除 XCTExpectFailure。
对于 flaky test(不稳定测试),严格模式会制造额外噪声。XCTExpectedFailure.Options 提供 isStrict,可以把它关闭。Swift 版本也支持在 XCTExpectFailure 里直接传 strict: false。
详细内容
1. 配置限制用 XCTSkip
(03:31)XCTSkip 适合表达配置限制。演讲中的例子是测试只支持 iPad。测试会执行到这一行,设备非 iPad 时跳过。
try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "Only supported on iPad")
关键点:
try:XCTSkipUnless会抛出 skip 异常,测试方法需要处理它。XCTSkipUnless(...):条件为 false 时跳过当前测试。UIDevice.current.userInterfaceIdiom == .pad:限制测试运行环境为 iPad。"Only supported on iPad":写入测试报告的跳过原因。
这个 API 解决的是环境不匹配。它不适合处理已知失败,因为跳过点之后的测试代码不会继续运行。
2. 用 XCTExpectFailure 标记已知失败
(04:31)XCTExpectFailure 用来管理已经登记的失败。调用后,测试继续执行,命中的失败会显示为预期失败。
XCTExpectFailure("<https://dev.myco.com/bugs/4923> myValidationFunction is returning false")
关键点:
XCTExpectFailure(...):声明当前测试中接下来的失败是已知失败。"<https://dev.myco.com/bugs/4923> ...":失败原因包含 bug 链接,Xcode 报告可以跳转到问题跟踪系统。myValidationFunction is returning false:原因文字说明具体失败点,避免只留下一个无上下文标记。
演讲里,加入这一行后,失败测试的红色 X 变成灰色 X。包含它的 test suite 变成绿色短横,表示通过但包含 skip 或 expected failure。
3. 用 scoped 写法限制影响范围
(07:14)如果只有某几行会触发已知失败,把它们放进闭包。闭包外出现失败时,XCTest 仍然按普通失败处理。
XCTExpectFailure("<https://dev.myco.com/bugs/4923> fix myValidationFunction") {
XCTAssert(myValidationFunction())
}
关键点:
XCTExpectFailure(... ) { ... }:创建一个只覆盖闭包内部代码的预期失败作用域。"<https://dev.myco.com/bugs/4923> fix myValidationFunction":记录问题链接和修复目标。XCTAssert(myValidationFunction()):真实断言仍然执行,失败时被闭包作用域捕获。- 闭包外代码:如果发生失败,会按普通失败进入报告。
演讲还说明,XCTExpectFailure 支持嵌套。失败会先匹配最近的调用点,如果 matcher 拒绝,再向外层调用传递。共享测试库中使用闭包写法,可以减少对调用方测试的影响。
4. 用 issueMatcher 精确匹配失败类型
(08:34)默认情况下,作用域内任何失败都会被捕获。为了发现新的问题,可以给 options 设置 issueMatcher,只接受符合条件的 XCTIssue。
let options = XCTExpectedFailure.Options()
options.issueMatcher = { issue in
return issue.type == .assertionFailure
}
XCTExpectFailure("<https://dev.myco.com/bugs/4923> fix myValidationFunction", options: options)
关键点:
let options = XCTExpectedFailure.Options():创建预期失败配置对象。options.issueMatcher = { issue in ... }:给配置对象设置失败匹配器。issue:XCTest 传入的XCTIssue,包含失败细节。return issue.type == .assertionFailure:只把断言失败当作预期失败处理。XCTExpectFailure(..., options: options):把匹配规则传给预期失败声明。
如果 matcher 拒绝某个失败,这个失败不会被当作预期失败。这样,旧问题继续降噪,新问题仍然能把测试变红。
5. 按平台关闭预期失败
(09:03)有些失败只出现在特定平台。演讲给出的例子是测试在 macOS 通过,在 iOS 失败。可以通过 options.isEnabled 关闭某些平台上的预期失败声明。
let options = XCTExpectedFailure.Options()
#if os(macOS)
options.isEnabled = false
#endif
XCTExpectFailure("<https://dev.myco.com/bugs/4923> fix myValidationFunction", options: options) {
XCTAssert(myValidationFunction())
}
关键点:
let options = XCTExpectedFailure.Options():准备配置对象。#if os(macOS):只在 macOS 编译分支内执行配置。options.isEnabled = false:在 macOS 上关闭这个预期失败声明。#endif:结束平台条件编译。XCTExpectFailure(..., options: options) { ... }:使用配置运行闭包内断言。XCTAssert(myValidationFunction()):失败只会在启用预期失败的平台上被捕获。
这种写法适合确定性的环境差异,例如平台、设备类型或系统版本差异。
6. 为不稳定测试关闭 strict 行为
(10:39)XCTExpectFailure 默认是 strict。声明了预期失败但没有捕获到失败时,XCTest 会报告 unmatched expected failure。这个行为适合稳定失败,因为它提示你删除过期标记。
let options = XCTExpectedFailure.Options()
options.isStrict = false
XCTExpectFailure("<https://dev.myco.com/bugs/4923> fix myValidationFunction", options: options) {
XCTAssert(myValidationFunction())
}
关键点:
let options = XCTExpectedFailure.Options():创建配置对象。options.isStrict = false:关闭 strict 行为。XCTExpectFailure(..., options: options) { ... }:使用非严格配置运行预期失败作用域。XCTAssert(myValidationFunction()):这次断言通过时,测试仍然通过。
Swift 还提供直接参数写法。
XCTExpectFailure("<https://dev.myco.com/bugs/4923> fix myValidationFunction", strict: false) {
XCTAssert(myValidationFunction())
}
关键点:
strict: false:直接关闭 strict 行为,不需要单独创建 options。- 闭包内部断言:失败时按预期失败处理,通过时不会制造 unmatched expected failure。
演讲把这种用法放在 nondeterministic failure(非确定性失败)场景中,包括 timing issue、ordering dependency 和 concurrency bug。
核心启发
-
做什么:给每个已知失败测试加上 bug 链接。 为什么值得做:XCTExpectFailure 的 reason 可以包含 URL,Xcode 测试报告能从预期失败跳回问题系统。 怎么开始:把失败测试中的临时注释改成
XCTExpectFailure("<bug URL> 失败原因")。 -
做什么:把共享测试工具函数里的已知失败改成 scoped 写法。 为什么值得做:闭包作用域只捕获内部失败,外层测试的新失败会继续变红。 怎么开始:用
XCTExpectFailure("原因") { ... }包住已知失败断言。 -
做什么:为 flaky test 建一个非严格预期失败清单。 为什么值得做:
isStrict = false避免偶发通过时产生 unmatched expected failure。 怎么开始:给相关测试创建XCTExpectedFailure.Options(),设置options.isStrict = false,再传给XCTExpectFailure。 -
做什么:只把特定失败类型标记为预期失败。 为什么值得做:
issueMatcher可以过滤XCTIssue,旧断言失败被降噪,新的崩溃或其他失败仍然暴露。 怎么开始:设置options.issueMatcher,先从issue.type == .assertionFailure这样的规则开始。 -
做什么:按平台拆分预期失败策略。 为什么值得做:演讲中的
options.isEnabled支持在某些平台关闭预期失败,适合 iOS 失败、macOS 已修复的测试。 怎么开始:用#if os(macOS)或其他平台条件设置options.isEnabled = false。
关联 Session
- Diagnose unreliable code with test repetitions — 用 Xcode test repetitions 反复运行不稳定测试,直到失败或满足停止条件。
- Explore Xcode Cloud workflows — 介绍 Xcode Cloud 的自动构建、分析、测试、归档和分发流程。
- Detect bugs early with the static analyzer — 用 Xcode 静态分析器在运行前发现代码问题。
- Explore Digital Crown, Trackpad, and iPad pointer automation — 介绍 Xcode 13 UI 测试中新加入的输入自动化能力。
评论
GitHub Issues · utterances