WWDC Quick Look 💓 By SwiftGGTeam
Migrate to Swift Testing

Migrate to Swift Testing

观看原视频

Highlight

Xcode 27 默认开启测试框架互操作性(test framework interoperability),允许在同一个测试 Target 中混写 XCTest 和 Swift Testing,开发者可以逐步迁移而非一次性重写所有测试代码。

核心内容

从”全量重写”到”逐步迁移”

去年 Xcode 16 推出 Swift Testing 时,很多团队心动但不敢动。一个成熟的项目往往有几千行 XCTest 代码,全部重写风险高、ROI 低。大多数团队的选择是:新项目用 Swift Testing,老项目继续维护 XCTest。

03:16)这场 session 的核心主张是:不要一次性修改所有老测试。每次只迁移几个你经常维护的测试,其余 XCTest 留在原地继续工作。同一个测试 Target 可以同时包含两套框架的测试文件,Xcode 会一起运行它们。

互操作性:两套框架的”翻译层”

05:48)互操作性(interoperability)是 Xcode 27 的新能力。它允许你在 Swift Testing 测试的函数体中调用 XCTest API(如 XCTFail),也允许在 XCTest 中调用 Swift Testing API(如 #expect)。框架在底层自动把问题报告给正确的测试运行器。

举个例子:你有一个被几十个 XCTest 调用的 helper 函数 assertUnique,内部用 XCTFail 报告失败。现在你想在 Swift Testing 测试中也复用这个 helper。互操作性让这件事直接可行——XCTFail 的调用会被转译为 Swift Testing 的 issue 报告。

四种互操作模式

07:43)Xcode 27 提供了四种模式来控制跨框架问题的处理方式:

  • Limited(Xcode 27 之前项目的默认值):XCTest 产生的跨框架问题显示为警告,测试仍通过
  • Complete(Xcode 27 新项目的默认值):跨框架问题保持为错误,测试会失败
  • Strict:XCTest 产生的跨框架问题直接触发 fatal error,强制你替换为 Swift Testing API
  • None:完全关闭互操作性,两个框架互不感知

09:05)建议策略:先用 Complete 模式让问题暴露出来,然后逐个替换。等迁移得差不多了,在 CI 中切换到 Strict 模式,禁止新的 XCTest API 调用混入 Swift Testing 测试。

参数化测试:替代嵌套循环

15:38)Swift Testing 的参数化测试(parameterized tests)解决了 XCTest 时代的一个常见痛点。假设你要测试每只鸟在 40 到 100 次拍翅膀下的表现,XCTest 的做法是写嵌套 for 循环:

func testBirdsFlapWings() async throws {
    for bird in Aviary.birds {
        for count in (40...100) {
            try await bird.flapWings(count: count)
        }
    }
}

问题很明显:循环内某一行失败时,你只知道是哪个 for 循环挂了,但不知道具体是哪只鸟、哪个次数。调试时需要加日志或挂调试器。

16:47)Swift Testing 的参数化测试把循环展开为独立的测试用例:

struct BirdTests {
    @Test(arguments: Aviary.birds, 40...100)
    func `Birds flap wings successfully`(bird: Bird, count: Int) async throws {
        try await bird.flapWings(count: count)
    }
}

每个参数组合变成一个独立的测试用例,在 Test Navigator 中可以展开查看。更重要的是,它们默认并行执行,总耗时大幅缩短。失败时,Xcode 直接标出是哪只鸟在哪个次数下失败,无需猜测。

Exit tests:测试崩溃路径

18:08)Swift Testing 的 exit test 让你可以测试那些会触发 preconditionFailurefatalError 的代码路径。框架会在子进程中运行测试体,主进程不受影响。

extension BirdTests {
    @Test func `Bird with empty name crashes`() async throws {
        await #expect(processExitsWith: .failure) {
            _ = Bird(name: "")
        }
    }
}

19:57)运行后,Code Coverage 中原本标红的 preconditionFailure 行会变成绿色——这条崩溃路径现在被测试覆盖了。

详细内容

在现有 XCTest 文件中添加 Swift Testing 测试

04:18)迁移的第一步不需要创建新文件。直接在现有的 .swift 测试文件末尾 import Testing,然后写一个 @Test 函数:

import Testing
@testable import DemoApp

@Test func `Default climate: tropical`() async throws {
    let fruit = Fruit(name: "Coconut")
    #expect(fruit.climate == .tropical)
}

关键点:

  • @Test 宏标记这是一个 Swift Testing 测试函数
  • 反引号(raw identifier)允许函数名包含空格和标点,测试名可以直接写成可读的自然语言
  • #expect 替代 XCTAssertEqual,失败时自动展开表达式显示左右两边的值
  • 不需要继承 XCTestCase,函数可以放在文件顶层

迁移 helper 函数:从 XCTFail 到 Issue.record

05:03)假设你有一个 XCTest 时代的 helper 函数:

func assertUnique(_ fruits: [Fruit], file: StaticString = #filePath, line: UInt = #line) {
    var uniqueNames = Set<String>()
    for name in fruits.map(\.name) {
        if !uniqueNames.insert(name).inserted {
            XCTFail("Duplicate name: \(name)", file: file, line: line)
        }
    }
}

10:12)把它改成同时支持 Swift Testing 的版本:

import Testing

func assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = #_sourceLocation) {
    var uniqueNames = Set<String>()
    for name in fruits.map(\.name) {
        if !uniqueNames.insert(name).inserted {
            Issue.record("Duplicate name: \(name)", sourceLocation: sourceLocation)
        }
    }
}

关键点:

  • XCTFail 替换为 Issue.record
  • file:line: 参数替换为 sourceLocation: SourceLocation
  • 更新后的 helper 同时被 XCTest 和 Swift Testing 调用时,两边都能正确报告失败位置

跳过测试的三种写法

13:10)XCTest 中用 XCTSkipIf 跳过测试。Swift Testing 提供了三种等效写法:

let isFall = false

// XCTest 方式
func testSwallowFallMigration() async throws {
    try XCTSkipIf(!isFall, "Wrong season for migration")
}

// Swift Testing:在测试体中调用 Test.cancel
func testSwallowFallMigration() async throws {
    if !isFall {
        try Test.cancel("Wrong season for migration")
    }
}

// Swift Testing 推荐:用 trait 把条件移出测试体
@Test(.enabled(if: isFall, "Wrong season for migration"))
func `Swallow fall migration`() async throws {
    // 测试逻辑
}

关键点:

  • Test.cancel 可以在测试体中动态决定是否跳过
  • .enabled(if:) trait 把跳过逻辑声明在函数签名上,测试体更干净
  • trait 方式让测试的”可执行条件”一目了然,不需要读函数体

用 #require 替代 continueAfterFailure

13:41)XCTest 中设置 continueAfterFailure = false 让测试在第一个失败断言处停止。Swift Testing 用 #require 宏实现同样的效果:

func testExample() async throws {
    #expect(Fruit.banana.climate == .temperate)

    try #require(Fruit.banana == Fruit.plantain)
    // 如果 #require 失败,会抛出错误,后续代码不会执行
}

关键点:

  • #expect 失败时继续执行后续断言
  • #require 失败时抛出错误,测试立即停止
  • 你可以自由选择哪些断言应该”硬停止”,哪些应该”软收集”

Swift Package 中控制互操作模式

12:15)Swift Package 项目的默认模式取决于 tools version:

  • swift-tools-version: 6.3 及更早:默认 limited 模式
  • swift-tools-version: 6.4 及更新:默认 complete 模式

在终端中可以用环境变量覆盖:

SWIFT_TESTING_XCTEST_INTEROP_MODE=strict swift test

这个命令在 CI 流水线中很有用——你可以把互操作模式设成 strict,确保团队成员不会在 Swift Testing 测试中偷偷调用 XCTest API。

核心启发

给现有项目的测试做”健康体检”

  • 做什么:扫描项目中的 XCTest helper 函数,把它们逐步迁移到 Swift Testing API
  • 为什么值得做:这些 helper 通常被大量测试调用,迁移后所有调用方自动受益;Issue.recordXCTFail 的报错信息更详细
  • 怎么开始:找一个被调用最频繁的 helper(如自定义的 assertSnapshotassertUnique),把 XCTFail 换成 Issue.record,跑一遍测试确认行为一致

用参数化测试消灭数据驱动测试的循环

  • 做什么:把项目中所有用 for 循环遍历测试数据的 XCTest,改写成 @Test(arguments:)
  • 为什么值得做:并行执行缩短 CI 时间;失败时精确到具体参数组合,省去调试时的猜测
  • 怎么开始:找出一个有嵌套循环的测试,把循环变量提取为函数参数,把循环范围放到 arguments:

给崩溃路径补上测试

  • 做什么:用 exit test 覆盖所有 preconditionFailurefatalErrorassertionFailure 的调用点
  • 为什么值得做:这些代码路径以前几乎无法测试,现在可以用 #expect(processExitsWith:) 安全验证;同时提升 Code Coverage
  • 怎么开始:在 Xcode 中打开 Code Coverage,找到标红的 preconditionFailure 行,写一个对应的 exit test

在 CI 中强制测试框架纯洁性

  • 做什么:在 CI 配置中加入 SWIFT_TESTING_XCTEST_INTEROP_MODE=strict
  • 为什么值得做:防止团队成员在新写的 Swift Testing 测试中混入 XCTest API,避免”缝合怪”代码积累
  • 怎么开始:在 .github/workflows/ 或 Jenkins pipeline 的 swift test 命令前加上环境变量

用 raw identifier 改善测试可读性

  • 做什么:把 testDefaultClimateIsTropical 这种驼峰命名改成 `Default climate: tropical`
  • 为什么值得做:测试名本身就是文档,带空格的句子比压缩的驼峰更易读;失败日志中显示的名字也更直观
  • 怎么开始:新建 Swift Testing 测试时直接用反引号包裹自然语言描述,不需要缩写

关联 Session

  • Go further with Swift Testing — Swift Testing 的进阶特性,包括自定义 traits、parallelization 控制和测试组织策略
  • Xcode 27 — Xcode 27 中测试相关的 UI 改进和 Coding Assistant 的迁移辅助功能
  • SwiftUI — SwiftUI 预览和 Swift Testing 的集成测试策略
  • SwiftData — 数据库层的测试最佳实践,配合 Swift Testing 做数据持久化验证

评论

GitHub Issues · utterances