-
迁移到 Swift Testing
了解如何借助测试框架之间的互操作性,在使用 XCTest 的同时放心地采用 Swift Testing。探索逐步引入高级测试功能的最佳做法和模式,以便加快开发速度并扩大测试覆盖面。
章节
- 0:07 - Introduction
- 1:08 - Swift Testing basics
- 2:50 - Migration strategy
- 5:48 - Test framework interoperability
- 7:43 - Interoperability modes
- 13:02 - Common migration patterns
- 15:34 - Parameterized tests
- 18:02 - Exit tests
- 20:04 - Next steps
资源
相关视频
WWDC26
WWDC24
-
搜索此视频…
你好 我叫Jerry 我是Swift Testing团队的工程师 今天 我来分享 如何比以往更轻松地 从XCTest迁移到Swift Testing
我们在Xcode 16中推出了Swift Testing 它是一个现代测试库 提供了简洁且富有表现力的接口 来编写测试 它也完全融入了 Swift生态系统 例如 它在设计时 就考虑了Swift并发性 可以并行运行测试用例 并以极快的速度输出结果
我将首先回顾 Swift Testing的基础构建块 并将其与XCTest中 的类似概念进行比较 接下来 我将把Swift Testing 添加到XCTest项目中 在此过程中 我将演示 如何复用现有代码 利用测试框架 互操作性功能
即使你不使用XCTest 也请继续收看 我将分享如何使用 Swift Testing的功能 大胆探索XCTest 从未踏足的领域
让我们从Swift Testing 的基础知识开始
我先导入 Swift Testing框架 以及通过testable import 访问内部类型 然后 我创建一个新函数 并用@Test宏对其进行标注 将其声明为测试
Swift支持原始标识符 由这些反引号标注 这让我可以在测试名称中 加入空格和标点 在测试主体中 我使用#expect宏 来创建 水果具有热带气候的预期 这就是创建新测试所需的全部内容 @Test和#expect宏是 大多数测试的核心构建块 如果你刚接触Swift Testing 或只是想温故知新 《Meet Swift Testing》 是绝佳的参考资料 它涵盖了更多构建块 和测试工作流程
#expect宏非常灵活 可替代许多XCTest断言 要替换XCTFail 即无条件失败断言 请改用Issue.record
使用Swift Testing 你将体验到用更少的代码 编写更强大 且更具表现力的测试 但在某些场景下 仍需继续使用XCTest UI自动化和性能测试API 仅在XCTest中可用 而在测试会抛出 Objective-C异常的代码时 必须使用同样以 Objective-C编写的XCTest 这是因为Swift代码 包括用Swift编写的XCTest 无法安全地处理这些异常 好了 我知道你现在 迫不及待地想写测试了 下面我来演示如何 无所顾虑地迁移到Swift Testing
我将介绍 分批迁移测试代码的策略
接下来 我将介绍 测试框架互操作性 这是一个强大的工具 可让我复用现有测试代码
为帮助你的迁移顺利进行 我将分享一些 常见的参考模式 好 来聊聊迁移策略
修改旧测试会引入风险 即便是小改动也不例外 因此 我会让 大多数XCTest保持原样 当我准备好时 我会每次只修改少量测试 重点关注 最常更新的那些
我现有的测试目标 可以包含两个框架的测试 因此 我可以对新测试 直接使用Swift Testing 但它们不能 放在XCTest类内部 好的 有了迁移策略 我已准备好付诸实践 我开发了一款应用 训练社区里的鸟类进行水果配送 就像鸟类随季节变化迁徙一样 我认为现在也是我 迁移到Swift Testing的时机
这是我的Fruit数据结构 所对应的XCTest套件 我最近为水果添加了气候信息 但尚未对其进行测试 我想使用Swift Testing 添加一些新测试 而且不用离开这个文件 就能完成 首先 我导入 Swift Testing框架
然后 我将在文件末尾 添加测试
我喜欢Swift Testing 能让测试置于套件之外 等有了更多测试 我随时可以再创建父套件 在Test navigator中 我注意到新测试已包含在内 现在 我打开Product菜单 并选择Test
太好了 我的新测试运行成功了
有些测试需要更多设置 这个测试检查 水果数组中的名称是否唯一 我最初编写这个测试时 用了多行代码 来设置断言
后来运行测试时 我发现 很难理解它为何失败 这个测试本想讲述一个故事 却迷失在大量代码之中 因此 我将这个多步断言 提取到了辅助函数中 现在 测试内容和方式 都更加清晰了 作为最后的修饰 我提供了文件和行号参数 并将其传入XCTFail
Xcode现在会将失败 归因到调用辅助函数的那一行
我想用Swift Testing 创建新测试 这些测试也会调用此辅助函数 并最终调用XCTFail 但XCTFail不属于Swift Testing 这正是测试框架 互操作性大显身手的地方 这一功能让你可以安全地 调用一个测试框架的API 同时在另一个框架的 测试主体中使用
考虑互操作性时 有两个方向需要思考 你可以调用XCTest API 在Swift Testing测试中 报告问题 这正是我复用assertUnique辅助函数时的情况 该函数封装了XCTFail 另一个方向 你可以调用Swift Testing API 在XCTest中报告问题 我稍后会解释这种情况
无论哪种情况 都会产生跨框架问题 报告问题的API与 调用它的测试 分属不同的框架 Xcode默认启用互操作性 来处理这些问题 让我来演示一下
我在testUniqueFruitNames中 调用assertUnique辅助函数 该辅助函数使用XCTFail报告失败 我创建了另一个Swift Testing测试 具有相同的主体 这个辅助函数应该报告相同的失败 但在这个测试中 这是一个跨框架问题
来比较运行此测试后的结果
嗯 我注意到测试通过了 但实际上我以为它会失败 就像上面的XCTest一样 测试失败有时也是好事 因为它意味着 辅助函数正在捕获Bug 不过这里有一些消息 我点击查看
互操作性产生了两个警告 由紫色三角形标注 由于这些不是错误 测试仍然通过 第一个警告告诉我 Lychee是重复名称 第二个警告 指示我将XCTFail替换为 Swift Testing的Issue.record
互操作性提供了 不同的模式 用于处理跨框架问题 我刚才介绍了limited模式 在此模式下 来自XCTest的 跨框架问题显示为警告 Xcode 27之前创建的测试计划 沿用limited模式 对于新项目 Xcode使用complete模式 在此模式下 相同的问题保持为错误
你可以随时通过 Test Plan Settings更改模式 可在Test Execution部分找到 来看看complete模式 如何改变测试结果 我来编辑测试计划
在这里 我筛选互操作性
并将该模式改为Complete
现在我回到"Unique fruit names" 测试并再次运行
现在 我的测试失败了 来查看消息
complete模式保留了 XCTFail创建的错误 它也保留了 提示我迁移的警告 可以把complete模式理解为 limited模式之上的一个层级 它将跨框架问题 从警告升级为错误 让你不那么容易遗漏它们
strict模式是complete模式 之上的再一步 对于来自XCTest的跨框架问题 strict模式会以致命错误 使测试立即停止 这有助于找到需要替换 XCTest API的地方 我已将模式更新为strict 现在 我再次运行测试
哇! 测试正好停在辅助函数中 调用XCTFail的位置 来查看这条消息
再一次 我收到了 替换XCTFail的提示 但我必须记住 我仍有XCTest在调用这个辅助函数 这就是互操作性也支持 来自Swift Testing的跨框架问题的原因 在所有模式下 这些问题均保持为错误 因此 你可以 调用Swift Testing API 在两个框架的测试中均可使用 替换XCTFail只需几个步骤 我先将XCTest的导入 替换为Swift Testing的导入
然后 将XCTFail替换为Issue.record sourceLocation替换 原始的文件和行号参数
更新辅助函数后 我将再次运行所有测试用例
我将使用CMD+U 即product test的快捷键
使用更新后的辅助函数 我的新测试 和原始XCTest 都如预期地失败了 感谢测试框架互操作性 我将XCTest API替换为 了Swift Testing API 而不改变测试的含义 还有一个我尚未介绍的模式 你可以将模式设置为none 以退出互操作性 退出后 Xcode将不再 报告跨框架问题 无论哪个方向 但这些问题 可以帮助发现App中的Bug 如果禁用互操作性 你将无法捕获这些Bug 因此 如果必须使用此模式 请仅临时使用 请优先选择complete或strict模式 complete模式将跨框架问题 保持为错误 是limited模式之上 有价值的提升 strict模式 顾名思义 很严格! 如果你想完全阻止 来自XCTest的跨框架问题 它非常合适 你也可以使用互操作性 及其不同模式 用于Swift Package项目 这是以swift-tools-version: 6.3 创建的项目示例 其中有一个测试产生 来自XCTest的跨框架问题 默认情况下 Swift 6.4工具链启用limited模式
当我使用swift test命令 运行此项目时 我注意到它将跨框架问题 报告为警告
要使用complete模式 请将软件包更新至swift-tools-version 6.4或更新版本
更新工具版本后 之前的问题现在变成了错误
随时使用环境变量 覆盖默认模式: SWIFT_TESTING_XCTEST_INTEROP_MODE 值填写模式名称的小写形式
最后 互操作性支持 一组有限的API 来自两个测试框架 我刚演示了XCTFail 和Issue.record API 在XCTest中 它还支持 所有其他测试断言 在Swift Testing中 它支持 两种预期宏: #expect和#require Swift Testing中的已知问题API 可将XCTest断言失败标记为已知 Test.cancel可以在XCTest中 跳过测试用例
在你的迁移过程中 可能会遇到一些常见模式 我将分享一些 应对这些模式的技巧
第一种模式是跳过测试 在XCTest中 你使用 XCTSkip API跳过测试
要将其替换为Swift Testing API 请使用Test.cancel 如果你在Swift Testing中 编写新测试 Test.cancel在那里也同样有效 但Swift Testing有traits 它们是可附加到 测试函数和套件的注解
将测试启用逻辑 移出测试主体 使用enabled或disabled trait
另一种常见模式 是在失败时中止 在XCTest中 将continueAfterFailure 属性赋值为false 这将在第一次断言失败时 停止测试 要替换为Swift Testing API 请使用#require宏 它在失败时抛出错误 中止测试 无需再设置continueAfterFailure 使用Swift Testing 你还可以选择哪些预期会中止测试 哪些不会
更多信息请参阅 开发者文档中的 《Migrating a test from XCTest》 它涵盖了更多场景 是迁移过程中的绝佳参考资料 Xcode的Coding Assistant 也了解这份文档 它可以帮助制定迁移策略 或审查你的工作 它甚至有一项技能 可以自动化部分迁移工作 哇 我们涵盖了很多内容! 总结一下 以下是如何无所顾虑地 迁移到Swift Testing
不要在准备好之前 被迫修改你的XCTest 专注于使用Swift Testing 编写新测试 依靠互操作性 你甚至可以复用 封装了XCTest API的辅助代码
在此过程中 你会发现 来自XCTest的跨框架问题 互操作性允许你 将XCTest API替换 为Swift Testing API 以解决这些问题 而Xcode 27默认 启用了互操作性 确保处理所有未来的 跨框架问题 通过升级到 complete或strict模式
现在 是时候将聚光灯 转向Swift Testing了
迁移到Swift Testing后 你可以使用新工具 来大幅提升测试能力 让我们从参数化测试开始
这些测试会用 不同的参数重复执行 每个参数都会成为 一个单独的测试用例 所有Swift Testing测试 包括参数化测试用例 默认并行运行 这比串行运行快得多 来看这个示例
我已将项目的Bird测试 迁移到Swift Testing 这检查每只鸟能否拍打翅膀 四十到一百次 但我无法为每种组合 都编写一个单独的测试 组合实在太多了 在我原始XCTest的主体中 我使用了嵌套循环 来生成所有组合 我来运行这个测试
有几件事引起了我的注意 测试花了一段时间才完成 可能是因为有很多组合 另外 测试失败了 但我甚至不知道哪只鸟失败了
如果我仍在使用XCTest 我必须捕获这些错误 或在附加调试器的情况下运行测试 我有个更好的主意 让我们把它变成参数化测试 我先从测试主体中 删除这些循环
然后将bird和count 定义为测试函数参数
我将birds和counts的输入 作为参数提供在test宏中
Swift Testing会将第一个 参数中的每只鸟 与第二个参数中的每个count配对 我再次运行测试
嘿! 这次 测试几乎 立刻就完成了 因为Swift Testing 并行运行了所有组合 在Test navigator中 我的参数化测试左侧 现在有一个展开箭头 我点击它来显示 所有正在测试的组合
哇 真是很多测试用例 好的 我还需要 找出失败的组合 在Test navigator中 我筛选失败的测试结果
啊 找到了! 燕子拍翅次数不能少于43次 使用参数化测试 大幅提升了我的测试执行效率 不仅获取结果的速度更快 还能清楚地知道 哪些测试输入失败了
接下来 我要展示如何通过 exit tests大幅提升测试覆盖率 请注意 exit tests仅在 macOS Linux FreeBSD和Windows上支持 我在寻找 需要测试覆盖率的代码 在Bird初始化器中发现了一些东西
如果新Bird的名称为空 我会用前置条件失败 来中止程序
我可以打开"Editor"菜单 来验证是否已测试过这一点 并显示"Code Coverage"
覆盖率注解将 前置条件标注为红色 这意味着我的测试 没有运行到此代码 exit test是 添加此覆盖率的完美工具
你使用#expect宏 定义exit test 提供预期的 进程退出条件 以及exit test的主体 当你启动测试时 Swift Testing会在子进程中 运行exit test主体 由于该代码是隔离的 它可以崩溃 而不影响其他测试 exit test等待 子进程完成 并检查退出状态 来判断测试成功或失败 我可以在测试套件的 这个扩展中添加新测试 我使用#expect宏 创建exit test 并指定进程 应以失败退出
在exit test主体内部 我将创建一个名称为空的Bird 这应该会崩溃 但它会被隔离 因为Swift Testing会在 单独的进程中运行此代码 现在 我运行所有测试
太好了 我的exit test通过了! 来再次检查覆盖率 我在Bird初始化器上右击 选择"Jump to Definition" 现在我们回来了 来再次查看覆盖率 覆盖率注解现在将 前置条件高亮显示为绿色 这意味着它现在已被测试到了!
这是两种提升测试能力 方式的快速预览 如果你有关于 Swift Testing如何改进的想法 我鼓励你来贡献! 这是因为 Swift Testing是开源的 它是GitHub上 SwiftLang组织的一部分 它现已支持 比以往更多的平台 今年开始完全支持FreeBSD
Testing Workgroup 负责管理该项目 并定期举行会议 任何Swift社区成员均可参加 新功能由Swift Evolution指导 实际上 互操作性就是其中之一! 欢迎加入Swift Forums 分享你的意见和想法 现在 轮到你像鸟一样 迁移到Swift Testing了! Xcode 27中的互操作性 让这一过渡比以往更轻松 在你的项目中试用 并探索其不同模式 在此过程中 你可以采用强大的工具 例如参数化测试和exit tests 如需深入了解 请查看WWDC 2024的 《Go further with Swift Testing》 祝你改造测试愉快!
-
-
1:12 - Name a test using a raw identifier
import Testing @testable import DemoApp @Test func `Default climate: tropical`() async throws { let fruit = Fruit(name: "Coconut") #expect(fruit.climate == .tropical) } -
5:03 - Wrap XCTFail in a test helper function
func testUniqueFruitNames() async throws { assertUnique(Market.fruits + [Fruit.lychee]) } // TestHelpers.swift 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 - Replace XCTFail with Issue.record in the test helper
import Testing func assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = ...) { var uniqueNames = Set<String>() for name in fruits.map(\.name) { if !uniqueNames.insert(name).inserted { Issue.record("Duplicate name: \(name)", sourceLocation: sourceLocation) } } } -
12:15 - Run Swift Package tests with the strict interoperability mode from Terminal
> SWIFT_TESTING_XCTEST_INTEROP_MODE=strict swift test -
13:10 - Common migration: skipping tests
let isFall = false // XCTest func testSwallowFallMigration() async throws { try XCTSkipIf(!isFall, "Wrong season for migration") // ... } // Test.cancel interoperability from Swift Testing func testSwallowFallMigration() async throws { if !isFall { try Test.cancel("Wrong season for migration") } // ... } // ✅ Prefer test trait in Swift Testing @Test(.enabled(if: isFall, "Wrong season for migration")) func `Swallow fall migration`() async throws { // ... } -
13:41 - Common migration: halting after test failures
func testExample() async throws { #expect(Fruit.banana.climate == .temperate) try #require(Fruit.banana == Fruit.plantain) XCTFail("This is never reached") } -
15:57 - Example of nested loops which can be converted into a parameterized @Test function
struct BirdTests { @Test func `Birds flap wings successfully`() async throws { for bird in Aviary.birds { for count in (40...100) { try await bird.flapWings(count: count) } } } } -
16:47 - Refactor nested loops into a parameterized @Test function
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) } } -
18:21 - Precondition check on empty input name in an initializer
// In `Bird.init(...)` if name.isEmpty { preconditionFailure("Bird name cannot be empty") } -
19:27 - Add coverage for precondition failure with exit test
extension BirdTests { @Test func `Bird with empty name crashes`() async throws { await #expect(processExitsWith: .failure) { _ = Bird(name: "") } } }
-
-
- 0:07 - Introduction
How to fearlessly migrate from XCTest to Swift Testing using the new interoperability feature.
- 1:08 - Swift Testing basics
A quick review of core Swift Testing building blocks — the @Test macro, #expect, and how they compare to XCTest assertions.
- 2:50 - Migration strategy
Covers the recommended incremental approach: leave existing XCTests in place, and start writing new tests in Swift Testing right away.
- 5:48 - Test framework interoperability
Introduces the interoperability feature that lets you safely call XCTest or Swift Testing API from within a test belonging to the other framework.
- 7:43 - Interoperability modes
Walks through the four interoperability modes — Limited, Complete, Strict, and None — and how to configure them in Xcode Test Plans and Swift packages.
- 13:02 - Common migration patterns
Covers practical patterns you will encounter during migration, including replacing XCTSkip with Test.cancel or traits, and continueAfterFailure with #require.
- 15:34 - Parameterized tests
Shows how to replace loop-based XCTest cases with Swift Testing parameterized tests for faster parallel execution and clearer failure reporting.
- 18:02 - Exit tests
Demonstrates how to use Swift Testing exit tests to cover code paths that call preconditionFailure or crash, running them safely in a child process.
- 20:04 - Next steps
Recaps the migration path, highlights Swift Testing open-source availability and cross-platform support, and encourages community participation.