modernize-tests

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Modernize Tests

测试现代化

Test modernization refers to two potential actions: migrating from XCTest to Swift Testing, and updating existing Swift Testing tests to use recommended patterns.
XCTests should be migrated to Swift Testing when possible. However, not all XCTests can be migrated to Swift Testing.
  • UI tests (those that use XCUIAutomation) cannot be written with Swift Testing, and must remain XCTests.
  • XCTests that use the
    measure { ... }
    family of APIs for performance measurement cannot be migrated. However, other test methods within an XCTestCase that do not use XCTest performance APIs can be migrated.
测试现代化包含两种操作:从XCTest迁移至Swift Testing,以及更新现有Swift Testing测试以使用推荐的模式。
在可行的情况下,应将XCTest迁移至Swift Testing,但并非所有XCTest都能完成迁移:
  • UI测试(使用XCUIAutomation的测试)无法用Swift Testing编写,必须保留为XCTest。
  • 使用
    measure { ... }
    系列API进行性能测量的XCTest无法迁移,但XCTestCase中不使用XCTest性能API的其他测试方法可以迁移。

Migration Reference

迁移参考

Imports

导入

Replace
import XCTest
with
import Testing
. A file can import both if it contains mixed test content during incremental migration.
When removing
import XCTest
, check whether the file uses Foundation types (URL, CharacterSet, ProcessInfo, Data, etc.). XCTest re-exports Foundation, so add
import Foundation
if needed.
import XCTest
替换为
import Testing
。在增量迁移期间,如果文件包含混合测试内容,可以同时导入两者。
移除
import XCTest
时,请检查文件是否使用Foundation类型(如URL、CharacterSet、ProcessInfo、Data等)。由于XCTest会重新导出Foundation,因此如果需要,请添加
import Foundation

Test Classes to Suites

测试类转测试套件

Remove
XCTestCase
inheritance. Prefer structs over classes:
  • final class FoodTruckTests: XCTestCase { ... }
    ->
    struct FoodTruckTests { ... }
移除
XCTestCase
继承。优先使用结构体而非类:
  • final class FoodTruckTests: XCTestCase { ... }
    ->
    struct FoodTruckTests { ... }

Move setUp/tearDown code to init/deinit

将setUp/tearDown代码迁移至init/deinit

Replace
override func setUp()
with
init()
(can be
async throws
). Replace
override func tearDown()
with
deinit
. If
deinit
is needed, use
actor
or
final class
instead of
struct
(since structs have no
deinit
). Change stored properties to not use implicitly-unwrapped optional types, and move their initial assignment from
setUp
to either be initialized inline or, if the initialization is complex, in an initializer.
struct MyTests {
    var fixture = Fixture()
    mutating func `Fixture behaves as expected`() {
        #expect(fixture.doSomething())
    }
}
Avoid pulling instance variables into function bodies; this can cause noise. Swift Testing reinvokes the initializer fresh before each test runs. If the test mutates an instance variable with value semantics, you may need to mark the test function
mutating
.
override func setUp()
替换为
init()
(可以是
async throws
)。将
override func tearDown()
替换为
deinit
。如果需要
deinit
,请使用
actor
final class
而非结构体(因为结构体没有
deinit
)。将存储属性改为不使用隐式解包可选类型,并将其初始赋值从
setUp
移至内联初始化,或者如果初始化逻辑复杂,则移至初始化器中。
struct MyTests {
    var fixture = Fixture()
    mutating func `Fixture behaves as expected`() {
        #expect(fixture.doSomething())
    }
}
避免将实例变量放入函数体内,这会产生冗余代码。Swift Testing会在每次测试运行前重新调用初始化器。如果测试使用值语义修改实例变量,可能需要将测试函数标记为
mutating

Test Methods

测试方法

Replace the
test
name prefix with the
@Test
attribute. If the resulting test name includes multiple camelCase words, use a raw identifier with the test name in sentence case.
  • func testEngineDoesNotStall() { ... }
    ->
    @Test func 
    Engine does not stall
    () { ... }
  • func testIgnition() { ... }
    ->
    @Test func ignition() { ... }
Test functions can be
async
,
throws
, or
async throws
, and can be isolated to a global actor with
@MainActor
.
test
名称前缀替换为
@Test
属性。如果生成的测试名称包含多个驼峰式单词,请使用原始标识符并将测试名称改为句子格式。
  • func testEngineDoesNotStall() { ... }
    ->
    @Test func 
    Engine does not stall
    () { ... }
  • func testIgnition() { ... }
    ->
    @Test func ignition() { ... }
测试函数可以是
async
throws
async throws
,也可以通过
@MainActor
隔离到全局actor。

Assertions to Expectations

断言转预期

When migrating a test from XCTest to Swift Testing, apply these mappings:
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)
try XCTUnwrap(x)
->
try #require(x)
There is no direct equivalent for
XCTAssertEqual(_:_:accuracy:)
; use floating point math directly.
将测试从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)
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)
try XCTUnwrap(x)
->
try #require(x)
XCTAssertEqual(_:_:accuracy:)
没有直接等效的API,请直接使用浮点数运算。

Errors

错误处理

When the error type is
Equatable
and the exact value is known, prefer to check the specific error value.
XCTAssertThrowsError(try f())
->
#expect(throws: (any Error).self) {
    try f()
}
XCTAssertThrowsError(try f()) { error in
    XCTAssertEqual(error, specificError)
}
->
#expect(throws: specificError) {
    try f()
}
XCTAssertThrowsError(try f()) { error in
    // Check error
}
->
let error = #expect(throws: (any Error).self) {
    try f()
}
// Check error
XCTAssertNoThrow(try f())
->
#expect(throws: Never.self) {
    try f()
}
当错误类型遵循
Equatable
且已知确切值时,优先检查特定错误值。
XCTAssertThrowsError(try f())
->
#expect(throws: (any Error).self) {
    try f()
}
XCTAssertThrowsError(try f()) { error in
    XCTAssertEqual(error, specificError)
}
->
#expect(throws: specificError) {
    try f()
}
XCTAssertThrowsError(try f()) { error in
    // Check error
}
->
let error = #expect(throws: (any Error).self) {
    try f()
}
// Check error
XCTAssertNoThrow(try f())
->
#expect(throws: Never.self) {
    try f()
}

continueAfterFailure

continueAfterFailure

By default
continueAfterFailure
is true, which means expectations do not halt the test run. Some XCTestCases set
continueAfterFailure = false
, which means the
XCTAssert
family of functions will throw Objective-C exceptions that halt the test execution.
When a test method sets
continueAfterFailure = false
, all subsequent assertions need to be
try #require(x)
instead of
#expect(x)
to preserve this behavior. When adding
try #require(x)
, add
throws
to the affected methods.
When
continueAfterFailure = false
is set in
setUp
, the conversion to
try #require(x)
must apply to all assertions in all test methods in that class.
默认情况下
continueAfterFailure
为true,这意味着预期不会终止测试运行。有些XCTestCase会设置
continueAfterFailure = false
,这意味着
XCTAssert
系列函数会抛出Objective-C异常以终止测试执行。
当测试方法设置
continueAfterFailure = false
时,所有后续断言需要改为
try #require(x)
而非
#expect(x)
,以保留该行为。添加
try #require(x)
时,请为受影响的方法添加
throws
如果在
setUp
中设置了
continueAfterFailure = false
,则该类中所有测试方法的所有断言都必须转换为
try #require(x)

Promote
Issue.record
/
XCTFail
to expectations

Issue.record
/
XCTFail
升级为预期

Wherever it is not disruptive, convert usage of
Issue.record
or
XCTFail
to #expect or #require, depending if the test exits after (taking
continueAfterFailure
into account).
In some cases, the source of the expectation itself is sufficient to explain the failure, and the comment would be redundant.
For example, the following structures should be converted as such:
guard let object = somethingOptional() else {
    Issue.record("Could not get object")
    return
}

guard object.isAvailable() else {
    Issue.record("Object not available")
    return
}

if !object.performOperation() {
    Issue.record("Failed to perform operation")
}
->
let object = try #require(somethingOptional(), "Could not get object")
try #require(object.isAvailable())
#expect(object.performOperation())
在不影响逻辑的情况下,将
Issue.record
XCTFail
的用法转换为#expect或#require,具体取决于测试是否会在失败后退出(需考虑
continueAfterFailure
的设置)。
在某些情况下,预期本身的来源足以解释失败原因,此时注释会显得冗余。
例如,以下结构应按如下方式转换:
guard let object = somethingOptional() else {
    Issue.record("Could not get object")
    return
}

guard object.isAvailable() else {
    Issue.record("Object not available")
    return
}

if !object.performOperation() {
    Issue.record("Failed to perform operation")
}
->
let object = try #require(somethingOptional(), "Could not get object")
try #require(object.isAvailable())
#expect(object.performOperation())

Asynchronous Expectations to Confirmations

异步预期转确认

Replace
XCTestExpectation
+
fulfill()
+
await fulfillment(of:)
with
confirmation()
:
swift
// Before
let exp = expectation(description: "...")
handler = { exp.fulfill() }
doWork()
await fulfillment(of: [exp])

// After
await confirmation("...") { confirm in
    handler = { confirm() }
    doWork()
}
For
assertForOverFulfill = false
with an
expectedFulfillmentCount
, use a range:
await confirmation("...", expectedCount: 10...) { confirm in ... }
XCTestExpectation
+
fulfill()
+
await fulfillment(of:)
替换为
confirmation()
swift
// Before
let exp = expectation(description: "...")
handler = { exp.fulfill() }
doWork()
await fulfillment(of: [exp])

// After
await confirmation("...") { confirm in
    handler = { confirm() }
    doWork()
}
对于设置
assertForOverFulfill = false
且指定
expectedFulfillmentCount
的情况,请使用范围:
await confirmation("...", expectedCount: 10...) { confirm in ... }

Skipping Tests

跳过测试

Replace
XCTSkipIf
/
XCTSkipUnless
with traits on the test or suite:
  • try XCTSkipIf(condition)
    ->
    @Test(.disabled(if: condition))
  • try XCTSkipUnless(condition)
    ->
    @Test(.enabled(if: condition))
Replace
throw XCTSkip("reason")
mid-test with
try Test.cancel("reason")
.
When a skip checks OS version or platform availability, replace it with an
@available
attribute on the test function instead of
.enabled(if:)
.
XCTSkipIf
/
XCTSkipUnless
替换为测试或套件的特性:
  • try XCTSkipIf(condition)
    ->
    @Test(.disabled(if: condition))
  • try XCTSkipUnless(condition)
    ->
    @Test(.enabled(if: condition))
将测试中途的
throw XCTSkip("reason")
替换为
try Test.cancel("reason")
当跳过检查依赖于OS版本或平台可用性时,请将其替换为测试函数上的
@available
属性,而非
.enabled(if:)

Known Issues

已知问题

Replace
XCTExpectFailure("...", ...) { ... }
with
withKnownIssue("...") { ... }
.
For intermittent failures, replace
.nonStrict()
option (or the shorthand
strict: false
parameter) with
isIntermittent: true
.
For conditional/matching: use
when:
and
matching:
parameters:
swift
withKnownIssue("...") {
    try riskyOperation()
} when: {
    shouldExpectFailure
} matching: { issue in
    issue.error != nil
}
XCTExpectFailure("...", ...) { ... }
替换为
withKnownIssue("...") { ... }
对于间歇性失败,请将
.nonStrict()
选项(或简写的
strict: false
参数)替换为
isIntermittent: true
对于条件/匹配:使用
when:
matching:
参数:
swift
withKnownIssue("...") {
    try riskyOperation()
} when: {
    shouldExpectFailure
} matching: { issue in
    issue.error != nil
}

Concurrency and Serial Execution

并发与串行执行

XCTest runs synchronous tests on the main actor and sequentially within a suite by default. Swift Testing runs all test functions on an arbitrary task and in parallel. Add
@MainActor
only if a test explicitly relied on main-actor isolation in its XCTest form, and add
@Suite(.serialized)
if tests depend on shared state.
默认情况下,XCTest在主actor上运行同步测试,并在套件内按顺序执行。Swift Testing在任意任务上并行运行所有测试函数。仅当测试在XCTest形式下明确依赖主actor隔离时,才添加
@MainActor
;如果测试依赖共享状态,请添加
@Suite(.serialized)

Attachments

附件

Replace
XCTAttachment
+
self.add(attachment)
with
Attachment.record(value)
. The attached type must conform to
Attachable
(automatic for
Codable
and
NSSecureCoding
types when Foundation is imported).
XCTAttachment
+
self.add(attachment)
替换为
Attachment.record(value)
。附件类型必须遵循
Attachable
协议(导入Foundation时,
Codable
NSSecureCoding
类型会自动遵循该协议)。

Modernization Guidelines

现代化指南

  • When migrating from XCTest, migrate one test class at a time. A file can contain both XCTest and Swift Testing tests during migration.
  • Prefer
    struct
    for suites unless
    deinit
    (tearDown) is needed, in which case use
    actor
    or
    final class
    .
  • Remove the
    test
    prefix from method names when adding
    @Test
    . For lengthier test names which read like a sentence, use raw identifier syntax to improve readability, e.g.
    @Test func 
    Authenticate, fetch summary, then check count
    () { ... }
    .
  • Use raw identifier syntax only for multi-word names that read like a sentence.
  • When migrating
    setUp
    , convert implicitly-unwrapped optional properties to non-optional properties initialized in-place, or in
    init
    if initialization is complex, may throw, or is async.
  • Look for explicit
    XCTFail
    /
    Issue.record
    calls that could be converted to
    #expect
    or
    #require
  • Do not change
    try #require
    calls into
    #expect
    ; this changes the behavior of tests.
  • Add
    @MainActor
    only to tests that explicitly relied on XCTest's implicit main-actor isolation. Do not add it unnecessarily.
  • Look for tests that loop over inputs or many repeated tests with the same logic and convert them to parameterized tests using
    @Test(arguments:)
    .
  • For suites with shared mutable state between tests, add
    @Suite(.serialized)
    and consider using
    actor
    or
    class
    instead of
    struct
    .
  • Do not introduce usage of underscore-prefixed symbols such as
    #_sourceLocation
    ; only use public API. For source locations, always use the full
    SourceLocation(fileID:filePath:line:column:)
    initializer.
  • If the test suite already uses #_sourceLocation, do not replace the existing usage as part of modernization.
  • Split this work over multiple agents if necessary if the modernization task is complex
  • 从XCTest迁移时,一次迁移一个测试类。迁移期间,一个文件可以同时包含XCTest和Swift Testing测试。
  • 优先为测试套件使用
    struct
    ,除非需要
    deinit
    (即tearDown),此时请使用
    actor
    final class
  • 添加
    @Test
    时,移除方法名称中的
    test
    前缀。对于较长且读起来像句子的测试名称,请使用原始标识符语法以提高可读性,例如
    @Test func 
    Authenticate, fetch summary, then check count
    () { ... }
  • 仅对读起来像句子的多词名称使用原始标识符语法。
  • 迁移
    setUp
    时,将隐式解包可选属性转换为非可选属性,在内联中初始化,或者如果初始化逻辑复杂、可能抛出错误或为异步,则在
    init
    中初始化。
  • 查找可转换为
    #expect
    #require
    的显式
    XCTFail
    /
    Issue.record
    调用。
  • 不要将
    try #require
    调用改为
    #expect
    ,这会改变测试的行为。
  • 仅对明确依赖XCTest隐式主actor隔离的测试添加
    @MainActor
    ,不要不必要地添加。
  • 查找循环处理输入或重复使用相同逻辑的测试,并使用
    @Test(arguments:)
    将其转换为参数化测试。
  • 对于测试之间存在共享可变状态的套件,添加
    @Suite(.serialized)
    ,并考虑使用
    actor
    class
    而非
    struct
  • 不要使用以下划线开头的符号,例如
    #_sourceLocation
    ;仅使用公开API。对于源位置,请始终使用完整的
    SourceLocation(fileID:filePath:line:column:)
    初始化器。
  • 如果测试套件已在使用
    #_sourceLocation
    ,则不要在现代化过程中替换现有用法。
  • 如果现代化任务复杂,必要时可将工作分配给多个Agent完成