Swift Testing:取代XCTest的框架,以及保留在XCTest中的内容
XCTest作为Apple自2013年以来的测试框架,正在被Swift Testing所取代。Apple尚未明确表示”已弃用”(XCTest仍在发布、仍可使用、仍承载着UI自动化和性能测试),但自WWDC 2024以来的每一场Apple会议、每一个Swift论坛话题以及每一个新的示例项目,都将Swift代码导向Swift Testing。该框架随Xcode 16+和Swift 6一同发布1,可在macOS、iOS、watchOS、tvOS、visionOS和Linux上运行2,并呈现出截然不同的思维模型:测试是函数,套件是类型,配置是特性,并行是默认设置。
迁移是渐进式的。Apple官方文档明确支持在同一目标中并行运行两个框架3,这意味着对于规模较大的代码库而言,正确的做法不是”全部重写”,而是”用Swift Testing编写新测试,并在接触旧测试时逐步迁移”。本文将介绍API接口、明确迁移边界(哪些内容保留在XCTest中),并涵盖取代XCTest类层次结构配置的特性词汇。
TL;DR
- Swift Testing用宏取代了XCTest基于类、依赖字符串类型的模型:
@Test、@Suite、#expect(...)和#require(...)4。 - 测试是自由函数或类型上的方法;套件是任何包含测试函数的类型(仅在指定显示名称或特性时才需要
@Suite)。 - 配置从类层次结构和命名约定迁移到特性:
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)5。 - 通过
@Test(arguments:)实现的参数化测试会产生一个父测试结果,每个参数对应一个子测试;默认并行执行6。 - 迁移采用并行方式,而非一次性切换。UI自动化(
XCUIApplication)和性能测试(XCTMetric)保留在XCTest中,因为Swift Testing尚未涵盖这些领域3。 - WWDC 2025新增了自定义附件(测试产物)和退出测试(验证预期在特定条件下终止的代码)7。
为何需要Swift Testing
任何维护过大型iOS测试套件的人对XCTest的痛点都不会陌生:
字符串类型断言。 XCTAssertEqual(a, b, "message")无法捕获原始的Swift表达式。当断言失败时,失败信息只告诉您运行时的值,而不告诉您是哪个表达式产生了它们。调试需要阅读测试源代码并重建上下文。
基于类的配置。 设置、清理和共享状态需要继承XCTestCase。禁用测试需要重命名(使发现模式无法找到它)或将其包装在#if DEBUG中。选择运行哪些测试需要谨慎的命名。
并发不匹配。 XCTest早于Swift Concurrency出现。XCTestExpectation加上wait(for:timeout:)是连接异步代码的遗留桥接方式;这种模式冗长且容易出错。现代Swift代码使用async/await;XCTest的惯用法显得过时。
参数化能力薄弱。 XCTest没有一流的参数化测试支持。开发者通过在测试方法内使用for循环来模拟(这会为多个用例产生一个测试结果),或编写代码生成器(从模板生成N个测试方法)。
Swift Testing解决了这些问题:
import Testing
@Test func userInitializesWithDefaults() {
let user = User()
#expect(user.name == "Anonymous")
#expect(user.preferences.isEmpty)
}
#expect捕获完整的表达式。当断言失败时,测试输出会显示user.name == "Anonymous"失败的原因是user.name为"Guest"。框架完成读取表达式的工作,因为宏可以看到语法树。无需”期望X,得到Y”的猜测。
宏
四个宏完成所有工作。
@Test和@Suite
@Test将函数标记为测试。该函数可以位于文件作用域中、结构体中、actor中、枚举中,以及Swift中函数可以存在的任何位置。框架会发现并运行被@Test标记的函数。
@Test func loginSucceedsWithValidCredentials() async throws {
let result = try await api.login(username: "alice", password: "valid")
#expect(result.isAuthenticated)
}
struct AuthenticationTests {
@Test func loginRejectsEmptyPassword() async throws {
let result = try await api.login(username: "alice", password: "")
#expect(!result.isAuthenticated)
#expect(result.error == .emptyPassword)
}
}
@Suite显式将类型标记为测试套件。当类型包含@Test函数时,标记是隐式的;只有在添加自定义显示名称或在套件级别应用特性时,才需要显式的@Suite4。
@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
@Test func loginSucceeds() async throws { ... }
@Test func loginFails() async throws { ... }
}
#expect和#require
#expect是标准断言。如果期望失败,测试会继续执行(失败被记录;后续断言仍会运行)。#require是快速失败的变体:如果要求失败,测试会立即停止。对于其失败会在后续断言中产生无意义错误的前置条件,请使用#require。
@Test func userPreferencesLoad() async throws {
let user = try #require(await store.fetchUser(id: 42))
// 如果fetchUser返回nil,测试在此停止。下一行
// 永远不会针对未解包的可选值运行。
#expect(user.preferences["theme"] == "dark")
#expect(user.preferences.count >= 1)
}
#require前面的try很重要:当要求满足时,#require返回解包后的值(在本例中为非可选的User),否则抛出错误。要使用#require进行解包,测试函数必须是throws。
参数化测试
@Test(arguments:)为每个参数运行一次测试函数。框架将每个参数报告为子测试,父测试作为汇总。
@Test(arguments: [
("alice", "password123", true),
("bob", "", false),
("", "password123", false),
("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
let result = try await api.login(username: username, password: password)
#expect(result.isAuthenticated == expectedSuccess)
}
框架默认并行运行四个参数。测试失败信息会标识哪个参数元组失败了,因此调试参数化失败不需要单独运行该测试。
对于独立的集合参数,@Test(arguments: arrayA, arrayB)运行笛卡尔积(每种组合)。对于成对序列,使用zip(arrayA, arrayB)并传递所得序列。
特性:作为一等公民值的配置
XCTest使用类层次结构、命名约定和Xcode测试计划来配置测试。Swift Testing使用特性:应用于@Test和@Suite声明的值5:
@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }
@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }
@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }
@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }
@Suite(.serialized)
struct DatabaseTests {
// 此套件中的所有测试按顺序运行。
// 当测试共享数据库夹具时很有用。
@Test func insertUser() { ... }
@Test func updateUser() { ... }
@Test func deleteUser() { ... }
}
特性词汇涵盖了XCTest需要自定义基础设施的场景:
.enabled(if:)和.disabled(_:)。 通过运行时表达式和可选原因实现条件启用。取代#if DEBUG块和重命名技巧。.serialized。 将套件或参数化测试标记为顺序执行。可继承:序列化的套件强制所有子测试串行执行6。.timeLimit(...)。 每个测试的时间限制。如果超过限制,测试将被终止并报告为失败。.tags(...)。 用于过滤的标签。在Xcode测试计划中配置包含/排除标签规则;该计划通过xcodebuild test -testPlan <name>运行。请注意,-only-testing:按测试标识符(目标、套件或函数)过滤,而非按标签过滤。.bug(...)。 将测试与bug ID及原因关联。该链接显示在测试报告中,并(对于已知失败的测试)防止失败阻塞CI。
也支持自定义特性。应用程序可以为项目特定需求定义自己的特性(例如.requiresKeychain、.skipIfOffline)。
默认并行执行
Swift Testing默认并行运行测试,包括参数化参数。默认设置很重要,因为大多数测试套件会随时间增长,而并行执行决定了测试运行是30秒还是5分钟。XCTest需要显式启用(在测试计划中切换测试并行选项);Swift Testing翻转了默认设置。
这意味着:测试必须独立。共享状态(数据库夹具、模拟文件系统、应用级单例)必须在每个测试中隔离(每个测试获得新的夹具),或标记为.serialized(套件按顺序运行)。那些会修改全局状态并假设串行执行的测试,是Swift Testing比XCTest更快暴露出来的bug。
迁移:并行进行,而非一次性切换
Apple关于迁移的官方立场是渐进式的3:
- 两个框架可在同一测试目标中共存。
- 可立即用Swift Testing编写新测试。
- 旧的XCTest测试无需更改即可继续工作。
- 在方便时(修改文件、新功能、重构)将XCTest测试迁移到Swift Testing。
有两种情况会无限期保留在XCTest中:
UI自动化测试。 XCUIApplication、XCUIElement、XCUIElementQuery,整个UI测试API,都仅限XCTest使用3。Swift Testing不提供替代方案。UI测试将保留在XCTest中,直到Apple扩展Swift Testing的覆盖范围。
性能测试。 XCTMetric、measure(metrics:)、性能基线,也仅限XCTest使用3。Swift Testing尚未提供等效功能。
对于其他所有内容(单元测试、集成测试、异步测试、参数化测试),Swift Testing是新代码的推荐默认选择。
WWDC 2025新增内容
WWDC 2025为Swift Testing增加了两项值得关注的功能7:
自定义附件。 测试可以将任意Attachable数据附加到其结果中以便故障分类。失败截图、诊断日志、生成的输入文件。附件会显示在Xcode的测试报告器和CI产物中。Sources/Testing/Attachments/Attachment.swift中的Attachment.record(_:named:sourceLocation:)API定义了接口;接受符合Attachable协议的值(Data、String、通过可选符合性的图像类型)。
@Test func reportLayoutAtFailure() async throws {
let result = await renderer.run(LayoutTestCase.complex)
if !result.passes {
Attachment.record(result.diagnostics, named: "diagnostics.txt")
Attachment.record(result.screenshotData, named: "failure-screenshot.png")
}
#expect(result.passes)
}
退出测试。 验证预期会终止的代码(调用exit()、抛出致命错误、触发前置条件失败)。XCTest没有干净的方式来测试这些;Swift Testing的退出测试在子进程中运行代码并验证终止行为。
@Test func parserExitsOnMalformedInput() async {
await #expect(processExitsWith: .failure) {
Parser.parse(malformedInput)
}
}
这两项新增都很务实。自定义附件解决了一个实际痛点(调试不稳定的CI失败);退出测试覆盖了单元测试接口中的真实空白。
何时XCTest仍是正确选择
新代码仍应使用XCTest的三种情况:
测试是UI自动化测试。 XCUIApplication.launch、点击和滑动交互、基于辅助功能标识符的查询。Swift Testing不涵盖UI自动化接口;新的UI测试使用XCTest。
测试是性能基准。 measure(metrics: [XCTClockMetric()])、性能基线、内存测量。Swift Testing没有等效功能;新的性能测试使用XCTest。
测试必须在不支持Swift Testing的平台上运行。 Swift Testing需要Swift 6 / Xcode 16+。必须针对早期工具链编译的测试(包含较旧Xcode版本的CI矩阵)需要XCTest,直到工具链跟上。
对于Swift 6+目标上的单元测试、集成测试、异步测试和参数化测试,Swift Testing是现代默认选择。迁移成本是真实的,但不迁移的成本会复利累积;今天编写的每一个新XCTest都是技术债务。
与Agent工作流的关联
测试是AI辅助代码生成所依据的契约。当agent编写Swift代码时,开发者(或agent本身)通过测试来验证更改是否正确。Swift Testing的特性使agent生成的代码更易验证:
- 表达式捕获。
#expect(user.score == calculator.compute(user))失败时显示比较的两边。读取失败信息的agent无需重新阅读测试文件即可修复bug。 - 参数化用例。 修复回归问题的agent知道哪个具体输入失败了。修复可以有的放矢。
- 基于特性的过滤。
.tags(.regression)注解让agent在修复后只运行回归测试;反馈循环更快。 - 自定义附件。 调试不稳定测试的agent可以附加失败输入产物,并在下次运行时读取它。
Apple开发的hooks一文介绍了在每次Claude Code编辑后运行测试的Stop hooks。Swift Testing更快、更并行、信息更丰富的失败反馈使该循环足够紧凑,让agent无需人工干预即可迭代。
此模式对iOS 26+应用意味着什么
三个要点。
-
新测试默认使用Swift Testing。 该框架更具表现力,默认并行运行,自动捕获失败上下文,并支持参数化测试作为一等概念。现有的XCTest代码继续工作;新代码使用现代默认选择。
-
将UI自动化和性能测试保留在XCTest中。 这是Apple的覆盖空白,而非迁移选择。在Swift Testing扩展到这些领域之前,这些测试将无限期保留在XCTest中。
-
使用特性作为配置词汇。
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)涵盖了XCTest需要类层次结构、命名约定和#if DEBUG块的场景。特性是组合的一等公民值;特性词汇正是让该框架感觉现代的关键。
完整的Apple生态系统集群:类型化的App Intents;MCP服务器;路由问题;Foundation Models;运行时与工具LLM区别;三个表面;单一真实来源模式;两个MCP服务器;Apple开发的hooks;Live Activities;watchOS运行时;SwiftUI内部机制;RealityKit的空间思维模型;SwiftData模式纪律;Liquid Glass模式;多平台发布;平台矩阵;Vision框架;Symbol Effects;Core ML推理;Writing Tools API;我拒绝写作的内容。中心枢纽位于Apple生态系统系列。关于更广泛的iOS与AI agent结合的背景,请参阅iOS Agent开发指南。
常见问题
Swift Testing会完全取代XCTest吗?
尚未。XCTest仍承载UI自动化测试(XCUIApplication)和性能测试(XCTMetric);Swift Testing不涵盖这些领域。对于单元测试、集成测试、异步测试和参数化测试,Swift Testing是新代码的推荐默认选择。两个框架可在同一目标中共存,互不冲突。
#expect和XCTAssertEqual之间的实际区别是什么?
#expect捕获完整的Swift表达式,并在失败时报告比较的两边(例如,user.score == calculator.compute(user)失败的原因是user.score = 80而calculator.compute(user) = 100)。XCTAssertEqual只知道运行时值和可选的消息字符串。表达式捕获正是让Swift Testing的失败信息无需重新阅读测试源代码即可自解释的原因。
参数化测试如何工作?
@Test(arguments: [...])为每个参数运行一次测试函数。框架将每个参数报告为子测试结果,父测试作为汇总。元组可以传递多个值。参数化测试默认并行运行;添加.serialized以强制顺序执行。
#require的用途是什么?
#require是#expect的快速失败变体。对于其失败会在后续断言中产生无意义错误的前置条件,请使用它。try #require(value)在满足条件时返回解包后的值,否则抛出错误,因此测试的其余部分可以直接使用解包后的值。要使用#require进行解包,测试函数必须是throws。
我可以在Linux上编写Swift Testing测试吗?
可以。Swift Testing是开源的2,并在Swift支持的每个平台上运行,包括Linux。服务器端Swift项目可以立即采用Swift Testing。该框架并不局限于Apple平台。
如何迁移现有的XCTest套件?
Apple的建议是渐进式的:用Swift Testing编写新测试,在接触旧测试时迁移它们3。没有自动转换器(模型差异足够大,自动化会很脆弱)。正确的迁移目标是单元测试和集成测试;UI测试和性能测试将无限期保留在XCTest中。
参考文献
-
Apple开发者:Swift Testing。Apple对Swift Testing随Xcode 16+和Swift 6一同采用的概述。 ↩
-
开源仓库:GitHub上的swiftlang/swift-testing。该框架的源代码,根据Apache License 2.0以及Swift项目的标准许可证提供。 ↩↩
-
Apple开发者文档:从XCTest迁移测试。Apple官方迁移指南,涵盖并行共存以及XCUIApplication / XCTMetric例外情况。 ↩↩↩↩↩↩
-
Apple开发者文档:Swift Testing。
@Test、@Suite、#expect、#require和特性词汇的框架参考。 ↩↩ -
Apple开发者文档:Trait。定义测试特性的协议,内置特性包括
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)。 ↩↩ -
Apple开发者:Meet Swift Testing(WWDC 2024 session 10179)和Go further with Swift Testing(WWDC 2024 session 10195)。涵盖参数化测试和并行性的入门会议。 ↩↩
-
WWDC 2025新增内容的开源仓库证据:自定义附件位于
Sources/Testing/Attachments/Attachment.swift,#expect(processExitsWith:)退出测试宏位于Sources/Testing/ExitTests/。两者都首次出现在WWDC 2025,并随Xcode 26+一同发布。 ↩↩