迁移到 Swift Testing
Migrate to Swift Testing
2026年6月10日
一句话判断
✅ Swift Testing 终于不用“推倒重来”了,官方提供的互操作性(Interoperability)让你能在同一个文件里无缝混写 XCTest 和 Swift Testing,老项目的迁移成本直接降维。
这场 Session 讲了什么
去年 WWDC 刚出 Swift Testing 时,大家都在叫好,但看着项目里几十万行的 XCTest 代码,只能叹气。重构测试代码的 ROI(投资回报率)太低,导致很多团队只能在新项目里尝尝鲜。
今年 Apple 直接给了“脚手架”。这场 Session 抛出了测试框架互操作性(Test framework interoperability),允许你在 XCTest 测试里调用 Swift Testing 的断言,或者在 Swift Testing 里复用老的 XCTAssert。你甚至不需要新建 Target,直接在老的 .swift 测试文件底部 import Testing 就能开始写新测试。
这对背着沉重历史包袱的中大型团队影响最大。你不用再纠结“要不要开新分支全量重构”,今天就能在刚写的业务逻辑里用上 @Test 宏和参数化测试(Parameterized tests),同时让 CI(持续集成)跑通所有新老测试。
值得深挖的点
互操作性(Interoperability)的妥协与智慧
Apple 这次没有搞“非此即彼”的逼宫,而是做了深度的底层桥接。当你在 Swift Testing 的 Helper 函数里调用 XCTFail,或者在 XCTest 里调用 #expect 时,框架在底层自动转换了上下文。比如把 XCTest 的 file:line: 自动映射为 Swift Testing 的 SourceLocation,确保报错堆栈不会断掉。
这种设计的 Trade-off 很明显:维护两套框架的上下文同步会增加运行时开销和边缘 Case。Apple 也清楚混写容易写出“缝合怪”代码,所以提供了 4 种互操作模式(Limited, Complete, Strict, None)。在过渡期你可以用 Complete 模式随便混写,等迁移差不多了,在 CI 里切到 Strict 模式,禁止在 Swift Testing 里调用 XCTest API,强制团队保持代码纯洁性。这种“先让你爽,再逼你规范”的策略,比直接一刀切高明得多。
参数化测试(Parameterized tests)对嵌套循环的降维打击
以前用 XCTest 测多组输入,只能写 for 循环。一旦中间某个 Case 挂了,默认后面的就不跑了;就算开了 continueAfterFailure,报错日志里也只能看到循环体内的行号,根本不知道是哪组参数出了问题,调试极其痛苦。
Swift Testing 的 @Test(arguments:) 直接把这个痛点秒了。它会在底层把参数组合展开成独立的测试用例。这意味着它们可以完美利用 Swift 的并发模型进行并行执行(Parallel execution),大幅缩短 CI 时间。更爽的是,如果某组参数失败,Test Navigator 里会清晰地列出具体是哪组参数挂了,报错精确到具体的输入值。这不仅是语法糖,而是实打实的工程效率提升。
代码片段
1. 在 Helper 函数中无缝替换 XCTFail
场景:你有一个被几十个老 XCTest 调用的断言 Helper 函数,现在想让新的 Swift Testing 也能复用,且报错行号准确。
import Testing
import XCTest
// 兼容两套框架的 Helper 函数
func assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = #_sourceLocation) {
var uniqueNames = Set<String>()
for name in fruits.map(\.name) {
if !uniqueNames.insert(name).inserted {
// 使用 Issue.record 替代 XCTFail,并传入 sourceLocation
// 这样报错会精准定位到调用 assertUnique 的那一行,而不是函数内部
Issue.record("发现重复的水果名称: \(name)", sourceLocation: sourceLocation)
}
}
}
坑:注意参数类型从 XCTest 的 file: StaticString = #filePath 变成了 Swift Testing 的 sourceLocation: SourceLocation = #_sourceLocation,老代码迁移时记得把签名换掉。
2. 用参数化测试干掉嵌套 For 循环
场景:测试鸟类在不同拍打次数下的飞行状态,以前写嵌套循环,现在直接参数化。
struct BirdTests {
// 传入两个集合,框架会自动生成笛卡尔积组合
// 每个组合都是一个独立的测试用例,支持并发执行
@Test(arguments: Aviary.birds, 40...100)
func `Birds flap wings successfully`(bird: Bird, count: Int) async throws {
// 如果某只鸟在拍打 50 次时失败,日志会明确标出 bird 和 count 的具体值
try await bird.flapWings(count: count)
}
}
坑:arguments 里的集合如果太大(比如几千个元素),会生成海量的测试用例导致内存暴涨,建议配合 .disabled() Trait 或缩小范围使用。
最佳实践
已有项目的迁移策略绝对是“增量替换”。别去动那些几年没改过、稳定运行的老 XCTest 代码。把精力放在高频修改的模块上,每次给老模块加新测试时,直接在文件底部用 @Test 写。对于新开的 Target,直接全面拥抱 Swift Testing,但记得把 UI 自动化(XCUITest)和性能测试(XCTestCase.measure)留在 XCTest 里,目前这两块 Swift Testing 还没完全接管。
实战中最容易踩的坑是 ObjC 异常处理。如果你的业务代码里混编了 Objective-C,且可能会抛出 ObjC 异常(NSException),千万别用 Swift Testing 去测。Swift 代码(包括 Swift Testing)无法安全捕获 ObjC 异常,这种场景必须老老实实写一个 ObjC 的 XCTest 类来兜底。
还有什么值得关注
- Exit tests 补齐了最后一块拼图:现在可以用
#expect(processExitsWith:)测试那些会触发preconditionFailure或直接 Crash 的代码路径,框架会把它扔到子进程里跑,主进程毫发无损。 - Raw identifiers 拯救命名强迫症:Swift 支持用反引号
`Default climate: tropical`作为函数名,终于不用写test_defaultClimate_isTropical这种又长又丑的驼峰命名了。 - 环境变量控制 CI 规范:在终端跑
swift test时,加上SWIFT_TESTING_XCTEST_INTEROP_MODE=strict,就能在 CI 阶段强制拦截那些在 Swift Testing 里偷偷调用 XCTest API 的不规范代码。