跳到正文

← 全部工具

给 Apple 开发者的免费参考

Swift Testing 参考与 XCTest 转换

Swift Testing 需要 Swift 6.0 和 Xcode 16 或更高。按 Apple 的迁移指南:XCTAssertEqual(a, b) 改为 #expect(a == b),XCTAssertNil(x) 改为 #expect(x == nil),try XCTUnwrap(x) 改为 try #require(x),XCTFail("…") 改为 Issue.record("…"),setUp 改为 init,tearDown 改为 deinit,expectation 改为 confirmation,XCTSkipIf 改为 .disabled(if:) 特征。UI 测试和性能测试仍用 XCTest。在下方粘贴测试类即可转换,无法转换的行会被标出并说明原因。下面还有 Apple 的完整对照表,以及 Swift Testing 每项功能的参考。

转换一个 XCTest 类

粘贴含有一个或多个 XCTestCase 类的 Swift 文件。转换在本页内完成,不会上传任何内容。

Swift Testing 版本

在上方粘贴 XCTestCase 类,即可看到 Swift Testing 版本。

XCTest 与 Swift Testing 对照

来自 Apple 的《Migrating a test from XCTest》。前几行就是 Apple 的断言对照表原样。 Apple 页面

XCTestSwift 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)
XCTAssertNotIdentical(x, y)#expect(x !== y)
XCTAssertGreaterThan(x, y)#expect(x > y)
XCTAssertGreaterThanOrEqual(x, y)#expect(x >= y)
XCTAssertLessThanOrEqual(x, y)#expect(x <= y)
XCTAssertLessThan(x, y)#expect(x < y)
XCTAssertThrowsError(try f())#expect(throws: (any Error).self) { try f() }
XCTAssertThrowsError(try f()) { error in … }let error = #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(x, y, accuracy: e)—没有对应写法。Apple 建议用 swift-numerics 中的 isApproximatelyEqual()。
import XCTestimport Testing迁移期间一个文件可以同时导入两者。
class FoodTruckTests: XCTestCasestruct FoodTruckTests去掉 XCTestCase 继承。Apple 建议用结构体或 actor,方便编译器检查并发安全。
func testEngineWorks()@Test func engineWorks()不再需要 test 前缀。XCTest 在主 actor 上运行同步测试,Swift Testing 不会,需要时请加 @MainActor。
override func setUp() async throwsinit() async throws
override func tearDown()deinit需要类或 actor(final class FoodTruckTests)。
continueAfterFailure = falsetry #require(…)
try XCTSkipIf(x) / try XCTSkipUnless(x)@Test(.disabled(if: x)) / @Test(.enabled(if: x))
throw XCTSkip("…")try Test.cancel("…")需要 Swift 6.3、Xcode 26.4 或更高。
XCTExpectFailure("…") { … }withKnownIssue("…") { … }
XCTExpectFailure("…", options: .nonStrict()) { … }withKnownIssue("…", isIntermittent: true) { … }
options.isEnabled / options.issueMatcherwithKnownIssue { … } when: { … } matching: { issue in … }
XCTExpectFailure(_:options:) (no closure)—没有直接对应写法。把整个测试体包进 withKnownIssue。
expectation(description:) / fulfill() / fulfillment(of:)await confirmation("…") { done in … done() }确认事件在闭包运行期间发生。使用范围次数需要 Swift 6.1。
expectedFulfillmentCount = 10confirmation(expectedCount: 10)
assertForOverFulfill = falseconfirmation(expectedCount: 10...)
(tests in a class run one after another)@Suite(.serialized)Swift Testing 默认并行运行套件里的测试。
XCTAttachment + add(_:)Attachment.record(…)
XCUIApplication (UI tests)—保留为 XCTest。Apple:Xcode 继续用 XCTest 编写 UI 测试。 Apple 页面
measure { … } (performance tests)—保留为 XCTest。Apple 的性能测试使用 XCTest 的 measure(_:)。 Apple 页面

Swift Testing 参考

每一项的作用、Apple 的示例,以及 Apple 在该符号页面上写明的最早 Swift 和 Xcode 版本。

@Test

把函数标记为测试。可以是全局函数,也可以是方法;名字不需要 test 前缀。第一个参数可以传显示名称。

@Test func engineWorks() {
  ...
}

@Test("Food truck exists") func foodTruckExists() { ... }

Swift 6.0,Xcode 16.0 Apple 页面

@Suite

把测试放进一个类型里分组。任何含有 @Test 方法的类型都是套件;用 @Suite 可以加显示名称或特征。

@Suite struct FoodTruckTests {
  @Test func engineWorks() { ... }
}

Swift 6.0,Xcode 16.0 Apple 页面

#expect

检查一个条件,失败时记录看到的值。测试会继续运行。

#expect(calculator.total(of: [3, 3]) == 7)
// Expectation failed: calculator.total(of: [3, 3]) == 7

Swift 6.0,Xcode 16.0 Apple 页面

#require

与 #expect 相同,但失败时抛错并停止测试。用于可选值时,会解包,或者停止测试。

let customer = try #require(Customer(id: 123))
try #require(engine.parts.first != nil)

Swift 6.0,Xcode 16.0 Apple 页面

#expect(throws:)

检查代码会抛错(或者用 Never.self 检查不会抛错)。返回值就是抛出的错误。

#expect(throws: (any Error).self) { try f() }
let error = #expect(throws: (any Error).self) { try f() }
#expect(throws: Never.self) { try f() }

Swift 6.0,Xcode 16.0 Apple 页面

Issue.record

无条件记录一次失败,用在没有合适条件的地方,比如 guard 的 else 分支。

guard case .electric = engine else {
  Issue.record("Engine is not electric")
  return
}

Swift 6.0,Xcode 16.0 Apple 页面

confirmation

确认某个事件在闭包运行期间发生。事件发生时像调用函数一样调用 confirmation。默认要求恰好一次。

await confirmation("…") { soldFood in
  FoodTruck.shared.eventHandler = { event in
    if case .soldFood = event { soldFood() }
  }
  await Customer().buy(.soup)
}

Swift 6.0,Xcode 16.0 Apple 页面

confirmation(expectedCount: 10...)

expectedCount 可以是数字,也可以是带下限的范围,比如 10...(至少十次)或 0..<1000。

await confirmation("…", expectedCount: 10...) { soldFood in
  ...
}

Swift 6.1,Xcode 16.3 Apple 页面

withKnownIssue

把闭包里的失败标记为已知问题,不会让测试失败。isIntermittent: true 允许某些运行通过。

withKnownIssue("Grill is out of fuel") {
  try FoodTruck.shared.grill.start()
}
withKnownIssue("Grill may need fuel", isIntermittent: true) { ... }

Swift 6.0,Xcode 16.0 Apple 页面

.enabled(if:)

只在条件为真时运行测试,否则在开始前跳过。

@Test("Ice cream is cold", .enabled(if: Season.current == .summer))
func isCold() async throws { ... }

Swift 6.0,Xcode 16.0 Apple 页面

.disabled

总是跳过测试,或在条件为真时跳过。备注会显示在测试结果里。

@Test("Food truck sells burritos", .disabled("We only sell Thai cuisine"))
func sellsBurritos() async throws { ... }

@Suite(.disabled(if: CashRegister.isEmpty))
struct FoodTruckTests { ... }

Swift 6.0,Xcode 16.0 Apple 页面

.timeLimit

测试运行超过时限就算失败。只能按分钟设置,至少一分钟。用在套件上时,对每个测试分别生效。

@Test(.timeLimit(.minutes(60)))
func serve100CustomersInOneHour() async { ... }

Swift 6.0,Xcode 16.0 还需要 iOS 16, macOS 13, tvOS 16, watchOS 9。 Apple 页面

.tags

给测试加标签,方便筛选和分组。用 @Tag 在 Tag 的扩展里声明标签。

extension Tag {
  @Tag static var legallyRequired: Self
}

@Test("Vendor's license is valid", .tags(.legallyRequired))
func licenseValid() { ... }

Swift 6.0,Xcode 16.0 Apple 页面

.serialized

让套件里的测试(或参数化测试的各个用例)逐个运行,而不是并行运行。

@Suite(.serialized)
class RefrigeratorTests {
  @Test func lightComesOn() throws { ... }
  @Test func lightGoesOut() throws { ... }
}

Swift 6.0,Xcode 16.0 Apple 页面

.bug

把测试与缺陷报告关联,可以用网址、编号或两者,还可加标题。

@Test("Food truck engine works", .bug("https://www.example.com/issues/12345"))
func engineWorks() async { ... }

@Test(.bug(id: "12345", "Forgot to buy more napkins"))
func hasNapkins() async { ... }

Swift 6.0,Xcode 16.0 Apple 页面

@Test(arguments:)

对每个值各运行一次测试。每个值是一个独立的测试用例,失败时会指出是哪个输入。用例默认并行运行。

@Test("All foods available", arguments: Food.allCases)
func foodAvailable(_ food: Food) async throws {
  let foodTruck = FoodTruck(selling: food)
  #expect(await foodTruck.cook(food))
}

Swift 6.0,Xcode 16.0 Apple 页面

async tests

测试函数可以是 async 和 throws 的,可以直接 await。

@Test func priceLookupYieldsExpectedValue() async {
  let mozarellaPrice = await unitPrice(for: .mozarella)
  #expect(mozarellaPrice == 3)
}

Swift 6.0,Xcode 16.0 Apple 页面

Test.cancel

提前结束正在运行的测试且不算失败,用来替代 throw XCTSkip。

if drawer.isEmpty {
  try Test.cancel("Cash register is empty")
}

Swift 6.3,Xcode 26.4 Apple 页面

Attachment.record

把一个值附加到测试结果里,用来替代 XCTAttachment。

let tortilla = Tortilla(diameter: .large)
Attachment.record(tortilla)

Swift 6.2,Xcode 26.0 Apple 页面

资料来源

Apple 的页面,读取于 2026 年 11 月 1 日:

它是怎么工作的

  • •转换在页面内读取你的 Swift 文本,只改写 Apple《Migrating a test from XCTest》文章描述的写法:导入、类、setUp 和 tearDown、测试名、断言对照表、XCTUnwrap、XCTFail、跳过、XCTExpectFailure,以及与唯一一个 wait 配对的 expectation。
  • •其他代码保持原样,在该行加上带编号的注释,并在结果下方说明原因。已改写但行为略有不同的行(confirmation、Test.cancel、特征)也会有说明。
  • •用到 XCUIApplication、measure 或 XCTest 指标的类保持为 XCTestCase,因为 Apple 用 XCTest 编写 UI 测试和性能测试。如果一个类里既有这类测试又有普通测试,会在同一个文件里拆成两部分:普通测试移到新的 Swift Testing 套件,UI 测试和性能测试留在原来的 XCTestCase 类里,两边都用到的 setUp、tearDown 或属性各复制一份并标出。只要还有代码需要,就保留 import XCTest。
  • •发布前,四个转换后的示例文件(包含结构体、final class、confirmation、特征、Test.cancel 和保留的性能测试)已用 Xcode 27.1(Swift 6.4)编译并运行。
  • •对照表和参考来自 Apple 的 Swift Testing 文档。版本号取自 Apple 每个符号页面上的标注,核对于 2026 年 11 月 1 日。

它能做什么,不能做什么

  • •不会编译或运行你的代码,也不了解你的类型。请构建结果并运行一次测试。
  • •跨多行的调用、带 file: 和 line: 的断言、带 accuracy: 的 XCTAssertEqual、带闭包的 XCTAssertThrowsError 只会被标出,不会改写。
  • •只有在同一代码块里恰好有一个对应的 wait(for:) 或 fulfillment(of:) 时,expectation 才会被转换。confirmation 不会等待,所以在稍后的回调里 fulfill 的代码需要重写。
  • •修改存储属性的测试会让套件变成 final class;不会把它们改写成局部变量。
  • •不会添加 @MainActor。XCTest 在主 actor 上运行同步测试,Swift Testing 不会,需要主线程的测试请自己加上。
  • •不转换 UI 测试和性能测试:Apple 没有为它们提供 Swift Testing 的替代品。混合的类里,它们留在 XCTestCase 类中,其他测试移出去。

常见问题

XCTAssertEqual 在 Swift Testing 里怎么写?
#expect(a == b)。Apple 的对照表把每个比较断言都对应到使用相应运算符的 #expect:XCTAssertNotEqual 对应 !=,XCTAssertGreaterThan 对应 >,XCTAssertIdentical 对应 ===,以此类推。带 accuracy: 的 XCTAssertEqual 没有对应写法,Apple 建议使用 swift-numerics 中的 isApproximatelyEqual()。
Swift Testing 里用什么替代 setUp 和 tearDown?
init() 和 deinit。Swift Testing 为每个测试创建一个新的套件实例,所以把准备工作放进 init(可以是 async 和 throws)。如果需要 deinit,Apple 说明要把套件声明为类或 actor,而不是结构体。
怎么替代 XCTestExpectation 和 wait(for:timeout:)?
使用 await confirmation("…") { event in … },在原来调用 fulfill() 的地方调用 event()。expectedFulfillmentCount 改为 expectedCount:,assertForOverFulfill = false 改为 10... 这样的范围。confirmation 不会等待:事件必须在闭包返回前发生,所以要在闭包内 await 相关工作。
UI 测试或性能测试可以用 Swift Testing 吗?
不可以。Apple 的 Xcode 文档说明 Xcode 继续用 XCTest 编写 UI 测试,性能测试使用 XCTest 的 measure(_:)。这些测试请放在 XCTestCase 类里;它们可以和 Swift Testing 测试放在同一个 target,甚至同一个文件里。如果一个类里同时有它们和普通测试,这里的转换会把类拆开:普通测试变成 Swift Testing 套件,UI 或性能测试留在 XCTestCase 类里。
用什么替代 XCTSkip 和 XCTSkipIf?
测试开始前就能确定的条件,用测试或套件上的 .disabled(if:) 或 .enabled(if:) 特征。要让正在运行的测试提前结束且不算失败,Apple 的替代写法是 try Test.cancel("…"),需要 Swift 6.3 和 Xcode 26.4 或更高。
必须一次转换所有测试吗?
不必。一个文件可以同时导入 XCTest 和 Testing,一个 target 也可以同时包含两种测试。可以分批迁移。Apple 的文章还介绍了互操作:使用 Swift 6.4 工具链且 swift-tools-version 为 6.4 时,Swift Testing 测试里失败的 XCTest 断言会让该测试失败(工具版本更低时只是警告,Swift 6.4 之前会被忽略),所以最好把断言改掉,而不要依赖互操作。

用到这个工具的指南

相关页面