test-modernizer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTest 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 with . A file can import both if it contains mixed test content during incremental migration.
import XCTestimport TestingWhen removing import XCTest, check whether the file uses Foundation types (URL, CharacterSet, ProcessInfo, Data, etc.). XCTest re-exports
Foundation, so add if needed.
import Foundation替换 为 。在增量迁移过程中,如果文件包含混合测试内容,可以同时导入两者。
import XCTestimport Testing移除时,检查文件是否使用Foundation类型(如URL、CharacterSet、ProcessInfo、Data等)。由于XCTest会重新导出Foundation,因此如果需要,请添加。
import XCTestimport FoundationTest Classes to Suites
测试类转为测试套件
Remove inheritance. Prefer over :
XCTestCasestructclass- ->
final class FoodTruckTests: XCTestCase { ... }struct FoodTruckTests { ... }
移除继承。优先使用而非:
XCTestCasestructclass- ->
final class FoodTruckTests: XCTestCase { ... }struct FoodTruckTests { ... }
setUp/tearDown to init/deinit
setUp/tearDown 转为 init/deinit
Replace with (can be ). Replace with . If is needed, use
or instead of (since structs have no ). Change stored properties to not use implicitly-unwrapped optional
types, and move their initial assignment from to either be initialized inline or, if the initialization is complex, in an initializer.
override func setUp()init()async throwsoverride func tearDown()deinitdeinitactorfinal classstructdeinitsetUpstruct 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 throwsoverride func tearDown()deinitdeinitactorfinal classstructdeinitsetUpstruct MyTests {
var fixture = Fixture()
mutating func `Fixture behaves as expected`() {
#expect(fixture.doSomething())
}
}避免将实例变量拉入函数体;这会增加冗余代码。Swift Testing会在每次测试运行前重新调用初始化方法。如果测试会改变具有值语义的实例变量,可能需要将测试函数标记为。
mutatingTest Methods
测试方法
Replace the name prefix with the attribute. If the resulting test name includes multiple camelCase words,
use a raw identifier with the test name in sentence case.
test@Test- ->
func testEngineDoesNotStall() { ... }Engine does not stall@Test func() { ... } - ->
func testIgnition() { ... }@Test func ignition() { ... }
Test functions can be , , or , and can be isolated to a global actor with .
asyncthrowsasync throws@MainActor将名称前缀替换为属性。如果生成的测试名称包含多个驼峰式单词,请使用原始标识符并将测试名称改为句子格式。
test@Test- ->
func testEngineDoesNotStall() { ... }Engine does not stall@Test func() { ... } - ->
func testIgnition() { ... }@Test func ignition() { ... }
测试函数可以是、或,也可以通过隔离到全局actor。
asyncthrowsasync throws@MainActorAssertions 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 ; use floating point math directly.
XCTAssertEqual(_:_:accuracy:)将测试从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:)Errors
错误处理
When the error type is and the exact value is known, prefer to check the specific error value.
EquatableXCTAssertThrowsError(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 errorXCTAssertNoThrow(try f())->
#expect(throws: Never.self) {
try f()
}当错误类型符合且已知确切值时,优先检查具体的错误值。
EquatableXCTAssertThrowsError(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 errorXCTAssertNoThrow(try f())->
#expect(throws: Never.self) {
try f()
}continueAfterFailure
continueAfterFailure
By default is true, which means expectations do not halt the test run.
Some XCTestCases set , which means the family of functions
will throw Objective-C exceptions that halt the test execution.
continueAfterFailurecontinueAfterFailure = falseXCTAssertWhen a test method sets , all subsequent assertions need to be
instead of to preserve this behavior. When adding , add to the affected methods.
continueAfterFailure = falsetry #require(x)#expect(x)try #require(x)throwsWhen is set in , the conversion to must apply
to all assertions in all test methods in that class.
continueAfterFailure = falsesetUptry #require(x)默认情况下为true,这意味着预期不会终止测试运行。
一些XCTestCase会设置,这意味着系列函数会抛出Objective-C异常以终止测试执行。
continueAfterFailurecontinueAfterFailure = falseXCTAssert当测试方法设置时,所有后续断言需要改为以保留此行为。添加时,请为受影响的方法添加。
continueAfterFailure = falsetry #require(x)try #require(x)throws如果在中设置了,则该类中所有测试方法的所有断言都必须转换为。
setUpcontinueAfterFailure = falsetry #require(x)Promote Issue.record
/XCTFail
to expectations
Issue.recordXCTFail将Issue.record
/XCTFail
升级为预期
Issue.recordXCTFailWherever it is not disruptive, convert usage of or to #expect or #require,
depending if the test exits after (taking into account).
Issue.recordXCTFailcontinueAfterFailureIn 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.recordXCTFail#expect#requirecontinueAfterFailure在某些情况下,预期本身的来源足以解释失败原因,此时注释会显得冗余。
例如,以下结构应按如下方式转换:
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 + + with :
XCTestExpectationfulfill()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()
}For with an , use a range:
assertForOverFulfill = falseexpectedFulfillmentCountawait confirmation("...", expectedCount: 10...) { confirm in ... }将 + + 替换为:
XCTestExpectationfulfill()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 = falseexpectedFulfillmentCountawait confirmation("...", expectedCount: 10...) { confirm in ... }Skipping Tests
跳过测试
Replace / with traits on the test or suite:
XCTSkipIfXCTSkipUnless- ->
try XCTSkipIf(condition)@Test(.disabled(if: condition)) - ->
try XCTSkipUnless(condition)@Test(.enabled(if: condition))
Replace mid-test with .
throw XCTSkip("reason")try Test.cancel("reason")When a skip checks OS version or platform availability, replace it with an attribute on the test function instead of .
@available.enabled(if:)将/替换为测试或套件的特性:
XCTSkipIfXCTSkipUnless- ->
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 with .
XCTExpectFailure("...", ...) { ... }withKnownIssue("...") { ... }For intermittent failures, replace option (or the shorthand parameter) with .
.nonStrict()strict: falseisIntermittent: trueFor conditional/matching: use and parameters:
when:matching:swift
withKnownIssue("...") {
try riskyOperation()
} when: {
shouldExpectFailure
} matching: { issue in
issue.error != nil
}将替换为。
XCTExpectFailure("...", ...) { ... }withKnownIssue("...") { ... }对于间歇性失败,将选项(或简写的参数)替换为。
.nonStrict()strict: falseisIntermittent: 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 only if a test explicitly relied on main-actor isolation in its XCTest form, and add if
tests depend on shared state.
@MainActor@Suite(.serialized)默认情况下,XCTest在主线程上运行同步测试,并在套件内按顺序执行。Swift Testing在任意任务上并行运行所有测试函数。仅当测试在XCTest形式下明确依赖主线程隔离时,才添加;如果测试依赖共享状态,请添加。
@MainActor@Suite(.serialized)Attachments
附件
Replace + with . The attached type must conform to (automatic for
and types when Foundation is imported).
XCTAttachmentself.add(attachment)Attachment.record(value)AttachableCodableNSSecureCoding将 + 替换为。附加的类型必须符合协议(当导入Foundation时,和类型会自动符合该协议)。
XCTAttachmentself.add(attachment)Attachment.record(value)AttachableCodableNSSecureCodingModernization 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 for suites unless
struct(tearDown) is needed, in which case usedeinitoractor.final class - Remove the 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.@TestAuthenticate, fetch summary, then check count@Test func.() { ... } - Also check existing functions for multi-word camelCase names and convert those to sentence-case raw identifiers.
@Test - Use raw identifier syntax only for multi-word names that read like a sentence.
- When migrating , convert implicitly-unwrapped optional properties to non-optional properties initialized in-place, or in
setUpif initialization is complex, may throw, or is async.init - Look for explicit /
XCTFailcalls that could be converted toIssue.recordor#expect#require - Do not change calls into
try #require; this changes the behavior of tests.#expect - Add only to tests that explicitly relied on XCTest's implicit main-actor isolation. Do not add it unnecessarily.
@MainActor - 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 and consider using
@Suite(.serialized)oractorinstead ofclass.struct - Do not use underscore-prefixed symbols such as ; only use public API. For source locations, always use the full
#_sourceLocationinitializer.SourceLocation(fileID:filePath:line:column:)
- 从XCTest迁移时,一次迁移一个测试类。迁移过程中,一个文件可以同时包含XCTest和Swift Testing测试。
- 优先为测试套件使用,除非需要
struct(即tearDown逻辑),此时请使用deinit或actor。final class - 添加时,移除方法名称中的
@Test前缀。对于较长的、类似句子的测试名称,使用原始标识符语法以提高可读性,例如testAuthenticate, fetch summary, then check count@Test func。() { ... } - 同时检查现有的函数,将多单词的驼峰式名称转换为句子格式的原始标识符。
@Test - 仅对类似句子的多单词名称使用原始标识符语法。
- 迁移时,将隐式解包可选属性转换为非可选属性,在内联处初始化,或者如果初始化逻辑复杂、可能抛出异常或为异步,则在
setUp中初始化。init - 寻找可以转换为或
#expect的显式#require/XCTFail调用。Issue.record - 不要将调用改为
try #require;这会改变测试的行为。#expect - 仅对明确依赖XCTest隐式主线程隔离的测试添加。不要不必要地添加该属性。
@MainActor - 寻找循环处理输入或重复使用相同逻辑的测试,将它们转换为使用的参数化测试。
@Test(arguments:) - 对于测试之间存在共享可变状态的套件,添加,并考虑使用
@Suite(.serialized)或actor而非class。struct - 不要使用以下划线开头的符号,例如;仅使用公开API。对于源代码位置,请始终使用完整的
#_sourceLocation初始化方法。SourceLocation(fileID:filePath:line:column:)