test-modernizer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test Modernizer

测试现代化工具

Apply when: user asks to modernize, update, migrate, supercharge, or convert their tests. XCTest should be migrated to Swift Testing when possible, existing Swift Testing tests should be evaluated to see if they could be better structured adopting newer features.
Do not apply when: user asks to write new tests from scratch (without existing XCTest code), user asks about XCTest features only, user only asks about test results or test running, user is asking to update tests to cover new functionality rather than updating the tests themselves, user is debugging test failures without mentioning migration, user has UI automation tests using XCUI* APIs (these cannot be migrated to Swift Testing).
适用场景:用户要求现代化、更新、迁移、增强或转换其测试用例。 在可能的情况下,应将XCTest迁移至Swift Testing,对于现有的Swift Testing测试,应评估是否可以采用更新的特性来优化其结构。
不适用场景:用户要求从零开始编写新测试(无现有XCTest代码)、仅询问XCTest特性、仅询问测试结果或测试运行、要求更新测试以覆盖新功能而非更新测试本身、在未提及迁移的情况下调试测试失败、使用XCUI* API的UI自动化测试(此类测试无法迁移至Swift Testing)。

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
struct
over
class
:
  • final class FoodTruckTests: XCTestCase { ... }
    ->
    struct FoodTruckTests { ... }
移除
XCTestCase
继承。优先使用
struct
而非
class
  • final class FoodTruckTests: XCTestCase { ... }
    ->
    struct FoodTruckTests { ... }

setUp/tearDown 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
而非
struct
(因为结构体没有
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)
以保留此行为。添加
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在主线程上运行同步测试,并在套件内按顺序执行。Swift Testing在任意任务上并行运行所有测试函数。仅当测试在XCTest形式下明确依赖主线程隔离时,才添加
@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
    () { ... }
    .
  • Also check existing
    @Test
    functions for multi-word camelCase names and convert those to sentence-case raw identifiers.
  • 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 use underscore-prefixed symbols such as
    #_sourceLocation
    ; only use public API. For source locations, always use the full
    SourceLocation(fileID:filePath:line:column:)
    initializer.
  • 从XCTest迁移时,一次迁移一个测试类。迁移过程中,一个文件可以同时包含XCTest和Swift Testing测试。
  • 优先为测试套件使用
    struct
    ,除非需要
    deinit
    (即tearDown逻辑),此时请使用
    actor
    final class
  • 添加
    @Test
    时,移除方法名称中的
    test
    前缀。对于较长的、类似句子的测试名称,使用原始标识符语法以提高可读性,例如
    @Test func 
    Authenticate, fetch summary, then check count
    () { ... }
  • 同时检查现有的
    @Test
    函数,将多单词的驼峰式名称转换为句子格式的原始标识符。
  • 仅对类似句子的多单词名称使用原始标识符语法。
  • 迁移
    setUp
    时,将隐式解包可选属性转换为非可选属性,在内联处初始化,或者如果初始化逻辑复杂、可能抛出异常或为异步,则在
    init
    中初始化。
  • 寻找可以转换为
    #expect
    #require
    的显式
    XCTFail
    /
    Issue.record
    调用。
  • 不要将
    try #require
    调用改为
    #expect
    ;这会改变测试的行为。
  • 仅对明确依赖XCTest隐式主线程隔离的测试添加
    @MainActor
    。不要不必要地添加该属性。
  • 寻找循环处理输入或重复使用相同逻辑的测试,将它们转换为使用
    @Test(arguments:)
    的参数化测试。
  • 对于测试之间存在共享可变状态的套件,添加
    @Suite(.serialized)
    ,并考虑使用
    actor
    class
    而非
    struct
  • 不要使用以下划线开头的符号,例如
    #_sourceLocation
    ;仅使用公开API。对于源代码位置,请始终使用完整的
    SourceLocation(fileID:filePath:line:column:)
    初始化方法。