modernize-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseModernize 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 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.
measure { ... }
测试现代化包含两种操作:从XCTest迁移至Swift Testing,以及更新现有Swift Testing测试以使用推荐的模式。
在可行的情况下,应将XCTest迁移至Swift Testing,但并非所有XCTest都能完成迁移:
- UI测试(使用XCUIAutomation的测试)无法用Swift Testing编写,必须保留为XCTest。
- 使用系列API进行性能测量的XCTest无法迁移,但XCTestCase中不使用XCTest性能API的其他测试方法可以迁移。
measure { ... }
Migration Reference
迁移参考
Imports
导入
Replace with . A file can import both if it contains mixed test content during incremental migration.
import XCTestimport TestingWhen removing , check whether the file uses Foundation types (URL, CharacterSet, ProcessInfo, Data, etc.). XCTest re-exports Foundation, so add if needed.
import XCTestimport Foundation将替换为。在增量迁移期间,如果文件包含混合测试内容,可以同时导入两者。
import XCTestimport Testing移除时,请检查文件是否使用Foundation类型(如URL、CharacterSet、ProcessInfo、Data等)。由于XCTest会重新导出Foundation,因此如果需要,请添加。
import XCTestimport FoundationTest Classes to Suites
测试类转测试套件
Remove inheritance. Prefer structs over classes:
XCTestCase- ->
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 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 classdeinitsetUpstruct 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)#expect(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())在不影响逻辑的情况下,将或的用法转换为#expect或#require,具体取决于测试是否会在失败后退出(需考虑的设置)。
Issue.recordXCTFailcontinueAfterFailure在某些情况下,预期本身的来源足以解释失败原因,此时注释会显得冗余。
例如,以下结构应按如下方式转换:
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在主actor上运行同步测试,并在套件内按顺序执行。Swift Testing在任意任务上并行运行所有测试函数。仅当测试在XCTest形式下明确依赖主actor隔离时,才添加;如果测试依赖共享状态,请添加。
@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.() { ... } - 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 introduce usage of underscore-prefixed symbols such as ; only use public API. For source locations, always use the full
#_sourceLocationinitializer.SourceLocation(fileID:filePath:line:column:) - 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(即tearDown),此时请使用deinit或actor。final class - 添加时,移除方法名称中的
@Test前缀。对于较长且读起来像句子的测试名称,请使用原始标识符语法以提高可读性,例如testAuthenticate, fetch summary, then check count@Test func。() { ... } - 仅对读起来像句子的多词名称使用原始标识符语法。
- 迁移时,将隐式解包可选属性转换为非可选属性,在内联中初始化,或者如果初始化逻辑复杂、可能抛出错误或为异步,则在
setUp中初始化。init - 查找可转换为或
#expect的显式#require/XCTFail调用。Issue.record - 不要将调用改为
try #require,这会改变测试的行为。#expect - 仅对明确依赖XCTest隐式主actor隔离的测试添加,不要不必要地添加。
@MainActor - 查找循环处理输入或重复使用相同逻辑的测试,并使用将其转换为参数化测试。
@Test(arguments:) - 对于测试之间存在共享可变状态的套件,添加,并考虑使用
@Suite(.serialized)或actor而非class。struct - 不要使用以下划线开头的符号,例如;仅使用公开API。对于源位置,请始终使用完整的
#_sourceLocation初始化器。SourceLocation(fileID:filePath:line:column:) - 如果测试套件已在使用,则不要在现代化过程中替换现有用法。
#_sourceLocation - 如果现代化任务复杂,必要时可将工作分配给多个Agent完成