← 所有文章

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自动化测试。 XCUIApplicationXCUIElementXCUIElementQuery,整个UI测试API,都仅限XCTest使用3。Swift Testing不提供替代方案。UI测试将保留在XCTest中,直到Apple扩展Swift Testing的覆盖范围。

性能测试。 XCTMetricmeasure(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+应用意味着什么

三个要点。

  1. 新测试默认使用Swift Testing。 该框架更具表现力,默认并行运行,自动捕获失败上下文,并支持参数化测试作为一等概念。现有的XCTest代码继续工作;新代码使用现代默认选择。

  2. 将UI自动化和性能测试保留在XCTest中。 这是Apple的覆盖空白,而非迁移选择。在Swift Testing扩展到这些领域之前,这些测试将无限期保留在XCTest中。

  3. 使用特性作为配置词汇。 .enabled(if:).disabled(_:).serialized.timeLimit(...).tags(...).bug(...)涵盖了XCTest需要类层次结构、命名约定和#if DEBUG块的场景。特性是组合的一等公民值;特性词汇正是让该框架感觉现代的关键。

完整的Apple生态系统集群:类型化的App IntentsMCP服务器路由问题Foundation Models运行时与工具LLM区别三个表面单一真实来源模式两个MCP服务器Apple开发的hooksLive ActivitieswatchOS运行时SwiftUI内部机制RealityKit的空间思维模型SwiftData模式纪律Liquid Glass模式多平台发布平台矩阵Vision框架Symbol EffectsCore ML推理Writing Tools API我拒绝写作的内容。中心枢纽位于Apple生态系统系列。关于更广泛的iOS与AI agent结合的背景,请参阅iOS Agent开发指南

常见问题

Swift Testing会完全取代XCTest吗?

尚未。XCTest仍承载UI自动化测试(XCUIApplication)和性能测试(XCTMetric);Swift Testing不涵盖这些领域。对于单元测试、集成测试、异步测试和参数化测试,Swift Testing是新代码的推荐默认选择。两个框架可在同一目标中共存,互不冲突。

#expectXCTAssertEqual之间的实际区别是什么?

#expect捕获完整的Swift表达式,并在失败时报告比较的两边(例如,user.score == calculator.compute(user)失败的原因是user.score = 80calculator.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中。

参考文献


  1. Apple开发者:Swift Testing。Apple对Swift Testing随Xcode 16+和Swift 6一同采用的概述。 

  2. 开源仓库:GitHub上的swiftlang/swift-testing。该框架的源代码,根据Apache License 2.0以及Swift项目的标准许可证提供。 

  3. Apple开发者文档:从XCTest迁移测试。Apple官方迁移指南,涵盖并行共存以及XCUIApplication / XCTMetric例外情况。 

  4. Apple开发者文档:Swift Testing@Test@Suite#expect#require和特性词汇的框架参考。 

  5. Apple开发者文档:Trait。定义测试特性的协议,内置特性包括.enabled(if:).disabled(_:).serialized.timeLimit(...).tags(...).bug(...)。 

  6. Apple开发者:Meet Swift Testing(WWDC 2024 session 10179)和Go further with Swift Testing(WWDC 2024 session 10195)。涵盖参数化测试和并行性的入门会议。 

  7. WWDC 2025新增内容的开源仓库证据:自定义附件位于Sources/Testing/Attachments/Attachment.swift#expect(processExitsWith:)退出测试宏位于Sources/Testing/ExitTests/。两者都首次出现在WWDC 2025,并随Xcode 26+一同发布。 

相关文章

Swift 新特性(2026):WWDC26 更新解读

来自 WWDC26 的 Swift 6.3 与 6.4:anyAppleOS 可用性、模块选择符、borrow/mutate 访问器、Iterable 协议、Swift Testing 互操作以及 MLX。

5 分钟阅读

五个Apple平台,三个共享文件:Return如何真正实现跨平台SwiftUI交付

Return运行在iPhone、iPad、Mac、Apple Watch和Apple TV上。在40个文件中,只有3个Swift文件在全部五个目标之间共享。其余都是重复的,而这是有意为之。

3 分钟阅读

循环工程:在验证成本低廉处,循环才能取胜

循环工程,对照 Boris Cherny 的完整访谈记录来检验:他点名的每一个循环,验证成本都很低。正是这一约束决定了什么值得自动化。

4 分钟阅读