← 所有文章

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+一同发布。 

相关文章

Five Apple Platforms, Three Shared Files: How Return Actually Ships Cross-Platform SwiftUI

Return runs on iPhone, iPad, Mac, Apple Watch, and Apple TV. Three Swift files are shared across all five targets out of…

18 分钟阅读

What SwiftUI Is Made Of

SwiftUI is a result-builder DSL on top of a value-typed View tree. Once the substrate is visible, AnyView, Group, and Vi…

17 分钟阅读

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 分钟阅读