认识 Swift Testing:全新的 Swift 测试框架
Meet Swift Testing
2024年6月10日
一句话判断
Swift Testing 是一个开源的全新 Swift 测试框架,用 @Test、#expect、Trait、Suite 四个构建块替代 XCTest 的传统模式,写法更 Swifty、失败信息更有用、组织方式更灵活,而且可以和 XCTest 混用。
这场 Session 讲了什么
Swift Testing 是今年推出的全新开源测试包。它不是 XCTest 的替代品,而是一个并行的选择——你可以同时在一个项目中使用两者。
Xcode 16 中新建测试目标时,Swift Testing 已经是默认选项。核心概念只有四个:@Test 标记测试函数,#expect 执行断言,Trait 定制测试行为,Suite 组织测试结构。
@Test 可以标注全局函数或类型中的方法,支持 async、throws、全局 actor 隔离。#expect 宏接受普通表达式和运算符,失败时自动捕获源码和子表达式的值,不需要学专门的断言 API。#require 是 #expect 的严格版本,用于可选值解包或需要提前终止测试的场景。
Trait 是第三个构建块,可以添加显示名称、关联 Bug、自定义标签、控制运行条件、设置时间限制等。Suite 通过 struct 来分组测试函数,每个实例测试方法会创建独立的 Suite 实例,避免状态共享问题。
框架支持所有主流平台包括 Linux 和 Windows,采用开放开发流程,社区可以参与塑造其发展方向。
值得深挖的点
#expect 宏的失败诊断有多好用
XCTest 的断言失败通常给你一个文件名和行号,你需要自己去查看当时变量的值。#expect 宏的做法完全不同——它会自动展开表达式并捕获每个子表达式的值。
比如 #expect(video.metadata == expectedMetadata) 失败时,结果视图会展开 metadata 的各个属性,让你逐一对比 duration、resolution 等字段哪里不等。#expect(numbers.contains(42)) 失败时会自动显示 numbers 数组的内容。
这种设计意味着你不需要在断言旁边加 print 语句来调试。表达式越复杂,这个特性的价值越大。而且你不需要学专门的 API——就是写普通的 Swift 表达式,框架帮你处理剩下的事。
Suite 的状态隔离模型
Suite 的设计有一个容易忽略的细节:每个实例 @Test 方法都会在一个全新的 Suite 实例上调用。这意味着你可以安全地在 Suite 中使用存储属性来共享初始化逻辑,而不用担心测试间的状态泄漏。
比如两个测试函数都需要创建一个 Video 对象。你可以把 let video = Video(...) 提取为 Suite 的存储属性,每个测试方法都能访问到独立的 video 实例。这比 XCTest 的 setUp 方法更优雅——不需要可选值,不需要隐式解包,就是一个普通的 let 常量。
Suite 可以标注 @Suite 属性显式声明,也可以省略——任何包含 @Test 函数的类型自动被视为 Suite。Suite 还可以有 init 和 deinit 来执行每个测试前后的逻辑。
代码片段
基本测试函数与 #expect 断言
import Testing
@testable import MyVideoApp
// @Test 标记测试函数,可以加显示名称
@Test("视频元数据验证")
func videoMetadata() async throws {
let video = Video(filename: "sample.mov")
let expected = Metadata(duration: 120, resolution: .hd)
// #expect 接受普通表达式,失败时自动展示子表达式值
#expect(video.metadata == expected)
// 也可以写更复杂的表达式
#expect(!video.metadata.duration.isZero)
#expect(video.metadata.resolution != .sd)
}
#require 安全解包可选值
@Test("播放列表第一首歌")
func playlistFirstTrack() async throws {
let playlist = Playlist(name: "精选")
playlist.add(Track(title: "晴天"))
// #require 解包可选值,如果为 nil 则测试立即停止
let first = try #require(playlist.tracks.first)
// 后续代码可以安全使用 first,不需要可选绑定
#expect(first.title == "晴天")
}
坑点:#require 必须用 try 调用。如果你忘记 try,编译器会报错,但这个错误信息可能不够直观,容易让人困惑。
用 Suite 组织测试并共享初始化
// Suite 通过 struct 分组测试
// 每个 @Test 方法都会获得独立的 VideoTests 实例
struct VideoTests {
// 存储属性:每个测试方法拿到独立的 video 实例
let video = Video(filename: "sample.mov")
@Test("内容分级为默认值")
func contentRating() {
#expect(video.contentRating == .general)
}
@Test("视频可以被标记为收藏")
func favorite() {
video.markAsFavorite()
#expect(video.isFavorite)
}
}
// Trait 示例:添加标签、关联 Bug、设置时间限制
@Test("导出功能", .tags("export"), .bug("https://bugs.example.com/123"), .timeLimit(.minutes(5)))
func exportVideo() async throws {
let exporter = VideoExporter()
let url = try await exporter.export(video)
#expect(FileManager.default.fileExists(atPath: url.path))
}
最佳实践
新项目建议直接用 Swift Testing。已有 XCTest 的项目可以逐步引入——两个框架可以在同一个测试目标中共存。迁移顺序建议:先在现有 XCTestCase 旁边添加新的 @Test 函数,验证感受一下 #expect 的诊断信息。等团队习惯了再把旧的 XCTestCase 逐步迁移过来。善用 Trait 来组织大型测试套件:.tags 做分类、.bug 关联问题追踪、.timeLimit 防止测试挂起。Suite 中的存储属性替代传统的 setUp,让代码更简洁。
还有什么值得关注
- Swift Testing 是开源项目,可以在 GitHub 上参与讨论和贡献
- 支持参数化测试,可以用不同参数重复运行同一个测试函数
- Xcode 16 的 Test Navigator 完整支持 Swift Testing 的层级展示和单独运行