Swift Testing 用标记了 @Test 的普通函数取代 XCTestCase 类,用两个宏取代整个 XCTAssert 系列:#expect(记录失败并继续执行)和 #require(抛出错误并停止)。setUp 和 tearDown 变成 init() 和 deinit。迁移可以一个文件一个文件地做,因为两个框架可以放在同一个测试 target 里,甚至同一个文件里。UI 测试和性能测试仍然留在 XCTest。想在浏览器里转换一个 XCTest 文件,可以试试我们的 XCTest 转 Swift Testing 工具。
Swift Testing 随 Swift 6.0 和 Xcode 16 提供。Apple 的 Xcode 测试页面说,Xcode 会继续包含 XCTest,用来编写 UI 测试。
导入与转换测试类
把 import XCTest 换成 import Testing。如果一个文件里还同时有两个框架的测试,就两个都导入。Apple 的迁移文章说,同一个源文件里可以同时包含用两种框架写的测试。
XCTestCase 的子类变成套件(suite):去掉对 XCTestCase 的继承即可。Apple 建议尽量用结构体或 actor 而不是类,这样编译器能更好地检查并发安全。给类型加 @Suite 是可选的,需要给套件起显示名或加特性时才加。
任何带 @Test 的函数都是一个测试,不需要以 test 开头,可以是 async、throws,也可以隔离到某个全局 actor。有一点要留意:XCTest 默认在主 actor 上运行同步测试方法,而 Swift Testing 在任意任务上运行测试函数。如果测试会碰到与界面相关的代码,要加上 @MainActor。
套件类型里的每个测试方法都在该类型的一个全新实例上运行,所以状态不会从一个测试漏到下一个。
改写前后
先看一个 XCTest 类,包含常见的 setUp、tearDown、相等判断、会抛错的调用和解包:
import XCTest
@testable import Cart
final class CartXCTests: XCTestCase {
var cart: Cart!
override func setUp() {
super.setUp()
cart = Cart()
}
override func tearDown() {
cart = nil
super.tearDown()
}
func testAddIncreasesCount() {
cart.add("tea")
XCTAssertEqual(cart.items.count, 1)
XCTAssertFalse(cart.items.isEmpty)
}
func testUnknownItemThrows() {
XCTAssertThrowsError(try cart.price(of: "gold"))
}
func testLookup() throws {
let price = try XCTUnwrap(cart.lookup("tea"))
XCTAssertEqual(price, 3)
}
}
下面是用 Swift Testing 写的同样的测试。带初始值的存储属性就顶替了 setUp,因为每个测试都有一个新实例:
import Testing
@testable import Cart
@Suite("Cart")
struct CartTests {
let cart = Cart()
@Test func addIncreasesCount() {
cart.add("tea")
#expect(cart.items.count == 1)
#expect(!cart.items.isEmpty)
}
@Test func unknownItemThrows() {
#expect(throws: CartError.self) { try cart.price(of: "gold") }
}
@Test func lookup() throws {
let price = try #require(cart.lookup("tea"))
#expect(price == 3)
}
}
准备与清理
Apple 给的规则是:用 init(),需要时再加 deinit,来代替 setUp() 和 tearDown()。init 可以是 async 和 throws。需要清理时,把套件声明成类或 actor,因为只有它们才能有 deinit。
final class Scratch: @unchecked Sendable {
static nonisolated(unsafe) var live = 0
init() { Scratch.live += 1 }
deinit { Scratch.live -= 1 }
}
@Suite(.serialized)
final class ScratchTests {
let scratch = Scratch()
deinit { /* clean up files or fixtures here */ }
@Test func isLive() { #expect(Scratch.live >= 1) }
}
断言对照表
下表来自 Apple 的迁移文章,只保留最常遇到的几行。
| XCTest | Swift Testing |
|---|---|
XCTAssert(x)、XCTAssertTrue(x) | #expect(x) |
XCTAssertFalse(x) | #expect(!x) |
XCTAssertNil(x) | #expect(x == nil) |
XCTAssertNotNil(x) | #expect(x != nil) |
XCTAssertEqual(x, y) | #expect(x == y) |
XCTAssertNotEqual(x, y) | #expect(x != y) |
XCTAssertIdentical(x, y) | #expect(x === y) |
XCTAssertGreaterThan(x, y) | #expect(x > y) |
XCTAssertLessThan(x, y) | #expect(x < y) |
XCTAssertThrowsError(try f()) | #expect(throws: (any Error).self) { try f() } |
XCTAssertNoThrow(try f()) | #expect(throws: Never.self) { try f() } |
try XCTUnwrap(x) | try #require(x) |
XCTFail("...") | Issue.record("...") |
XCTAssertEqual(_:_:accuracy:) 没有直接对应的写法。Apple 的建议是用 swift-numerics 包里的 isApproximatelyEqual() 做带误差的数值比较。#require 也是 continueAfterFailure = false 的替代:它抛出的是 Swift 错误,其他测试仍会继续运行。
参数化测试
XCTest 里用循环时,看不出是哪个输入失败了;Swift Testing 则为每个输入跑一个测试用例,并分别报告。把一个集合传给 arguments: 即可。Apple 的文档还展示了遵循 CaseIterable 的枚举(传入 allCases,新增一个 case 不用改测试就会被覆盖)和整数范围。
@Test("Known prices", arguments: [("tea", 3), ("cake", 5)])
func price(item: String, expected: Int) throws {
#expect(try cart.price(of: item) == expected)
}
参数化测试的各个用例默认彼此并行运行。
特性(Traits)
特性是传给 @Test 或 @Suite 的附加参数。套件上的特性会被其中的测试继承。
import Foundation
import Testing
extension Tag {
@Tag static var pricing: Self
}
@Suite(.tags(.pricing))
struct TraitTests {
@Test(.disabled("Waiting for the new price list"))
func futurePrices() { #expect(Bool(false)) }
@Test(.bug("https://example.com/issues/42", "Price rounds wrong"), .timeLimit(.minutes(1)))
func slowPath() { #expect(Cart().lookup("tea") == 3) }
@Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_SLOW"] == nil))
func enabledByDefault() { #expect(true) }
}
.disabled(_:)带着说明跳过一个测试,.enabled(if:)只在条件成立时运行。它们取代了XCTSkip、XCTSkipIf和XCTSkipUnless。.bug(_:_:)把测试关联到问题追踪里的一条记录。.tags(_:)接收用@Tag宏声明的Tag值,用来给测试分组和筛选。.timeLimit(_:)让运行过久的测试失败,可用版本为 iOS 16、macOS 13 和 watchOS 9。.serialized让套件里的测试依次运行。Swift Testing 默认并行运行测试,而 XCTest 默认逐个运行同一个类里的测试,所以如果测试共享全局状态,要加上.serialized。
对于预期会失败的情况,withKnownIssue { ... } 取代 XCTExpectFailure。Apple 指出,不带闭包的 XCTExpectFailure 没有直接对应物;改用 withKnownIssue 包住整个测试体即可。
用 confirmation 测试异步回调
能直接 await 异步函数就直接 await,或者用 continuation 包装带回调的函数。Apple 的文章推荐这样做,而不是用 expectation。对于自己到来的事件,比如稍后才触发的回调,用 confirmation(_:expectedCount:),它对应 XCTestExpectation。
@Test func changeHandlerFires() async {
await confirmation("onChange called") { changed in
cart.onChange = { changed() }
cart.add("tea")
}
}
@Test func changeHandlerFiresTwice() async {
await confirmation(expectedCount: 2) { changed in
cart.onChange = { changed() }
cart.add("tea")
cart.add("cake")
}
}
和 expectation 有两点不同。confirmation 不会阻塞或等待:事件必须在 confirmation 返回之前发生,否则就会记录一个问题。默认值是恰好确认一次,这点和 XCTestExpectation 一样。如果要表达"至少几次",可以传一个有下界的范围,比如 1... 或 0 ..< 100;expectedCount: 0 用来检查某件事从未发生。
仍留在 XCTest 里的测试
- UI 测试。 Apple 的 Xcode 测试页面说,Xcode 保留 XCTest 用于控制 App 界面的 UI 测试。Apple 的"添加测试"指南也把 UI 测试描述为
XCTestCase的子类。 - 性能测试。 Apple 的性能测试指南说它们使用 XCTest 框架,在
XCTestCase里调用measure。Swift Testing 没有对应功能。 - 依赖 XCTest 专有 API 的测试,比如带 accuracy 参数的
XCTAssertEqual。
两者并行使用
Apple 的指南说,Swift Testing 和 XCTest 可以在同一个测试 target 里构建,并共存于同一个测试包中,所以不需要新建 target。有时间再逐个文件转换即可。
辅助函数可以共用。启用互操作后,被 Swift Testing 测试调用的辅助函数里的 XCTAssert 会被报告为失败;被 XCTest 方法调用的辅助函数里的 #expect 会让该方法失败。默认模式取决于工具链:工具链低于 6.4 时是 none;6.4 及以上时是 limited,如果包的 swift-tools-version 也是 6.4 及以上,则是 complete。想自己指定模式,可以设置环境变量 SWIFT_TESTING_XCTEST_INTEROP_MODE。更稳妥的做法是先把共用的辅助函数改成 #expect,Apple 的文章把这描述为可以按自己的节奏逐步迁移测试基础设施的做法:
func assertUnique(_ elements: [Int]) {
#expect(Set(elements).count == elements.count)
}
@Test func uniqueElements() { assertUnique([1, 2, 3]) }
final class UniqueXC: XCTestCase {
func testUnique() { assertUnique([1, 2, 3]) }
func testMeasure() { measure { _ = (0..<1000).map { $0 * 2 } } }
}
来源
以下页面均阅读于 2026 年 11 月 1 日。
- Migrating a test from XCTest(Swift Testing;对照表、准备与清理、互操作、已知问题)
- Testing(Xcode)
- Adding tests to your Xcode project(Xcode)
- Writing and running performance tests(Xcode)
- Implementing parameterized tests
- Organizing test functions with suite types
- Traits,含
timeLimit(_:)、disabled(_:sourceLocation:)、enabled(if:_:sourceLocation:)、bug(_:_:)和tags(_:) - Testing asynchronous code
Apple 的 API 和默认行为可能变化,请以上述页面的最新内容为准。