从 XCTest 切到 Swift Testing:一次实战迁移笔记
切入点:别急着删 XCTest,先并行跑起来
WWDC 2024 之后,Swift Testing 成了 Xcode 16 里的官方首选。但作为从 XCTest 时代摸爬滚打过来的老工程,最快的落地方式是让两套框架在同一个 target 里共存,而不是一次性重写所有用例。Swift Testing 在设计上明确支持与 XCTest 并存,这意味着你可以按文件、按模块逐步迁移,风险可控。实测下来,Xcode 16 的 Test Navigator 会同时识别两类测试,运行时报错信息也不混淆,迁移窗口期很舒服。
核心语法的三个差异点
1. 用 @Test 替代 func testXXX()
XCTest 靠方法名前缀识别测试,Swift Testing 则用宏显式标记。示例:
import Testing
@Test("登录流程-正常路径")
func testLoginFlow() {
// 断言逻辑
}
这里的字符串参数是可选显示名,不写的话默认用函数名。注意 #expect 和 XCTest 的 XCTAssertEqual 不同,它不区分具体断言类型,统一收一个表达式或闭包。
2. #expect 与 #require 的分工
XCTest 时代用 continueAfterFailure = false 控制失败后是否继续,Swift Testing 的对应物是 #require。它的语义是:失败即抛错,立即中止当前测试。
let user = try #require(User(name: "Alice"))
// 下面代码只有在 user 非 nil 时才执行
而 #expect 失败后测试会继续往下跑,适合收集多条失败信息。这里有个坑:#require 要求表达式返回 Optional,否则编译报错。解包后的变量在闭包外可用,但要注意 Swift 的流敏感作用域——如果 #require 在闭包内,解包值的作用域会受限,不确定时就拆成两步写。
3. @Suite 组织测试
类似 XCTest 里的 XCTestCase 子类,但 @Suite 不是强制继承关系。它可以挂在 struct 上,更符合 Swift 的值语义,也能传参。一个坑:@Suite 里的 init 如果抛错,整个 suite 会被标记失败,而不是单个测试。所以初始化逻辑里别放断言。
迁移时容易踩的边界
- traits 不是过滤器的全部:
@Tag、.enabled(if:)、.serialized这些 traits 看起来很美好,但.enabled(if:)的条件是编译期常量或运行期表达式,不能依赖外部文件状态,否则作用域会不明确。 - 默认并行运行 + 随机顺序:Swift Testing 默认并行 + 随机,这对有共享状态的测试是致命的。如果老用例里有用单例或静态变量的,要么加
.serializedtrait,要么 refactor 掉。我建议第一时间加--disable-parallelism跑一遍基线,再逐个放开。 swift test命令陷阱:命令行下,部分 Swift 版本默认仍走 XCTest runner,需要显式加参数。我们 CI 里用的是swift test --enable-swift-testing,不加的话新测试可能静默不执行,这是最容易发现不了的问题。
边界:哪些场景别迁移
Swift Testing 目前不支持性能测试和 UI 测试,这两类用例继续留在 XCTest。另外,Android/Wasm 平台支持标注是实验性的,生产环境别赌。如果工程里大量使用动态 Xcode Cloud 集成或旧版 xcodebuild 脚本,也建议先小范围试点,因为 xcodebuild 对 swift-testing 的结果解析格式跟 XCTest 不完全一致。
收尾建议
迁移顺序:先挑一个无共享依赖的小模块跑通 → 跑 CI 对比失败率 → 再动核心业务层。别为了"新"而全量重写,XCTest 和 Swift Testing 并存是被官方支持的路径,也是目前最稳妥的。