WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift Testing

Meet Swift Testing

观看原视频

Highlight

Swift Testing 是今年推出的全新开源测试包,可以和 XCTest 在同一个 target 中共存,Xcode 16 新建测试目标时已默认选中它。


核心内容

写测试最烦的不是写测试本身,是 XCTest 的样板代码太多。每个测试必须继承 XCTestCase,方法名必须以 test 开头,断言要记一堆 XCTAssertXxx 的函数签名,setUp/tearDown 的生命周期还得小心翼翼。这些都不是”测试逻辑”,是框架的负担。

Swift Testing 的做法是把这些框架噪音降到最低。你只需要两样东西:@Test 标记函数,#expect 写断言。不需要继承任何类,不需要记住十几种断言函数,#expect 接受普通 Swift 表达式,失败时自动捕获子表达式的值。Xcode 16 中新建测试目标时,Swift Testing 已经是默认选项。

框架的四个构建块分别是:@Test 标记测试函数,#expect/#require 执行断言,Trait 定制测试行为,Suite 组织测试结构。每个测试函数在 Suite 中运行时都会创建独立的实例,避免了 XCTestCase 中常见的状态共享问题。整个框架采用开放开发流程,社区可以在 GitHub 上参与贡献和提案。


详细内容

写第一个测试

导入 Testing 模块,用 @Test 标记全局函数,用 #expect 断言(01:54):

import Testing
@testable import DestinationVideo

@Test func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}

关键点:

  • import Testing 引入框架,@testable import 是 Swift 语言自身的特性,用于访问 internal 成员
  • @Test 可以标注全局函数,也可以标注类型中的方法,支持 asyncthrows、全局 actor 隔离
  • #expect 接受普通表达式和运算符,失败时自动捕获源码和左右两边的值

#require:提前终止测试

#require#expect 的严格版本,用于可选值解包或需要提前终止测试的场景(02:35):

@Test(.tags(.formatting)) func formattedDuration() async throws {
    let videoLibrary = try await VideoLibrary()
    let video = try #require(await videoLibrary.video(named: "By the Lake"))
    #expect(video.formattedDuration == "0m 19s")
}

关键点:

  • #require 需要加 try,表达式为 false 或可选值为 nil 时抛出错误并终止测试
  • 适合后续代码依赖于前面结果的场景——值不存在时继续跑没有意义

Trait:定制测试行为

Trait 是第三个构建块,可以给测试添加描述信息、控制运行条件、修改行为(06:06):

@Test(.disabled("Due to a known crash"),
      .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
    // ...
}

关键点:

  • .disabled(...) 禁用测试但保证代码仍能编译,比注释掉函数更安全
  • .bug(...) 关联问题追踪链接,在 Xcode 16 的 Test Report 中可直接点击打开
  • .enabled(if:) 控制运行条件,条件不满足时标记为 skipped
  • .tags(...) 给测试打标签,可跨文件、跨 target 分组,在 Test Navigator 中按标签查看和运行

Suite:组织测试结构

用 struct 包装测试函数,形成 Suite(07:18):

@Suite(.tags(.formatting))
struct MetadataPresentation {
    let video = Video(fileName: "By the Lake.mov")

    @Test func rating() async throws {
        #expect(video.contentRating == "G")
    }

    @Test func formattedDuration() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "By the Lake"))
        #expect(video.formattedDuration == "0m 19s")
    }
}

关键点:

  • Suite 鼓励用 struct,值语义避免状态共享;每个实例 @Test 方法会创建独立的 Suite 实例
  • 存储属性替代传统的 setUpinit 替代 setUpdeinit 替代 tearDown(deinit 需要用 class 或 actor)
  • @Suite 属性可以指定 display name 和 trait,子 Suite 和内部测试会继承

参数化测试

把重复的测试函数合并成一个参数化测试(14:07):

@Test("Number of mentioned continents", arguments: [
    "A Beach",
    "By the Lake",
    "Camping in the Woods",
    "The Rolling Hills",
    "Ocean Breeze",
    "Patagonia Lake",
    "Scotland Coast",
    "China Paddy Field",
])
func mentionedContinentCounts(videoName: String) async throws {
    let videoLibrary = try await VideoLibrary()
    let video = try #require(await videoLibrary.video(named: videoName))
    #expect(!video.mentionedContinents.isEmpty)
    #expect(video.mentionedContinents.count <= 3)
}

关键点:

  • 函数签名加参数,@Testarguments: 指定输入值
  • Test Navigator 中每个参数值显示为独立的测试条目,可单独重跑和调试
  • 比 for…in 循环好:每个参数的结果独立可见,支持并行执行,可以单独重跑失败的参数

与 XCTest 的关系

两者可以在同一个测试目标中共存(17:36)。UI 自动化(XCUIApplication)和性能测试(XCTMetric)仍需用 XCTest。迁移建议:多个结构相似的 XCTest 方法可合并为一个参数化测试;只有一个测试方法的 XCTestCase 可迁移为全局 @Test 函数。不要在 Swift Testing 测试中调用 XCTAssert 函数,也不要在 XCTestCase 中调用 #expect


核心启发

  1. 用参数化测试消除重复的测试代码:项目中大量”同一逻辑、不同输入”的测试函数是维护负担。把它们合并成参数化测试,新增测试用例只需在 arguments 数组里加一行。怎么开始:找到项目中函数名靠后缀区分的测试(如 testVideoAtestVideoB),提取公共逻辑为参数化函数。

  2. 用 .tags 替代文件目录来组织测试:传统做法是按文件夹分组测试,但跨模块的关联很难表达。.tags 可以跨文件、跨 target 标记,在 Test Navigator 中按标签筛选和运行,还能在 Test Report 中看到同一标签下多个测试同时失败的关联洞察。怎么开始:为每个核心功能/子系统定义一个 Tag,逐步给现有测试打标。

  3. 用 .disabled + .bug 替代注释掉的测试:注释掉的测试不参与编译,代码可能早已腐坏。.disabled 保证代码始终能编译,.bug 关联问题链接让禁用原因可追溯。怎么开始:搜索项目中所有注释掉的测试函数,替换为 .disabled("原因").bug("链接")

  4. 新项目直接用 Swift Testing,旧项目逐步引入:两个框架可以在同一个 target 中共存,不需要创建新 target。先在现有 XCTestCase 旁边添加新的 @Test 函数体验 #expect 的诊断信息,等团队适应后再逐步迁移。怎么开始:在现有测试目标中 import Testing,写一个新的 @Test 函数跑通流程。


关联 Session

评论

GitHub Issues · utterances