Meet Swift Testing
SwiftUI & UI Frameworks 进阶 20m

认识 Swift Testing:全新的 Swift 测试框架

Meet Swift Testing

2024年6月10日

在 Apple 官方观看视频

一句话判断

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 的层级展示和单独运行
WWDC 2024