研发协作
需要将 XCTest 测试套件迁移/现代化到 Swift…
把现有 XCTest/Swift Testing 测试套件现代化为 Swift Testing 风格,包括导入、套件结构、断言、错误、异步、跳过、已知问题、并发和附件等迁移。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
需要将 XCTest 测试套件迁移/现代特定
解决思路
- 一次迁移一个 XCTest 测试类;迁移期间同一文件可同时包含 XCTest 和 Swift Testing 测试。
- 用 import Testing 替换 import XCTest;如果是增量迁移且文件内混合测试内容,可以同时 import 两者。
- 移除 import XCTest 后检查文件是否使用 Foundation 类型(URL、CharacterSet、ProcessInfo、Data 等);因为 XCTest 会 re-export Foundation,如有使用则添加 import Foundation。
- 移除 XCTestCase 继承,优先把测试类改为 struct,例如 final class FoodTruckTests: XCTestCase { ... } -> struct FoodTruckTests { ... }。
- 把 override func setUp() 替换为 init()(可以是 async throws)。
- 把 override func tearDown() 替换为 deinit。
- 如果需要 deinit(tearDown),不要用 struct,改用 actor 或 final class。
- 把存储属性从隐式解包可选类型改为非可选类型,并把初始化赋值从 setUp 移到属性内联初始化;如果初始化复杂、可能 throw 或 async,则移到 init 中。
- 不要在函数体内拉入实例变量,以免造成噪音;Swift Testing 会在每个测试运行前重新调用初始化器。
- 如果测试会以值语义修改实例变量,把该测试函数标记为 mutating。
- 用 @Test 属性替换测试方法名的 test 前缀。
- 如果去掉 test 前缀后的测试名包含多个 camelCase 单词,使用原始标识符(反引号)并写成句子式大小写。
- func testEngineDoesNotStall() { ... } -> @Test func `Engine does not stall`() { ... }。
- func testIgnition() { ... } -> @Test func ignition() { ... }。
- 测试函数可以是 async、throws、async throws,并可用 @MainActor 隔离到全局 actor。
- 断言迁移 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:) 没有直接等价物;直接使用浮点数学进行比较。
- 当错误类型是 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() },然后检查 error。
- 错误迁移 XCTAssertNoThrow(try f()) -> #expect(throws: Never.self) { try f() }。
- 默认 continueAfterFailure 为 true,expectations 不会中止测试运行。
- 有些 XCTestCase 设置 continueAfterFailure = false,这会让 XCTAssert 系列抛出 Objective-C 异常并中止测试执行。
- 当某个测试方法设置 continueAfterFailure = false 时,后续所有断言都要用 try #require(x) 替代 #expect(x) 以保留行为;添加 try #require(x) 时给受影响方法添加 throws。
- 当 continueAfterFailure = false 设置在 setUp 中时,try #require(x) 转换必须应用到该类的所有测试方法中的所有断言。
- 在不造成干扰的情况下,把 Issue.record 或 XCTFail 转换为 #expect 或 #require,具体取决于测试之后是否退出(考虑 continueAfterFailure)。
- 有些情况下 expectation 的来源本身已足以解释失败,注释会变得冗余。
- 例如 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())。
- 用 confirmation() 替换 XCTestExpectation + fulfill() + await fulfillment(of:)。
- 异步 expectation 迁移示例:原先 let exp = expectation(description: "..."); handler = { exp.fulfill() }; doWork(); await fulfillment(of: [exp]),改为 await confirmation("...") { confirm in handler = { confirm() }; doWork() }。
- 当 assertForOverFulfill = false 且使用 expectedFulfillmentCount 时,使用范围:await confirmation("...", expectedCount: 10...) { confirm in ... }。
- 用测试或套件上的 traits 替换 XCTSkipIf/XCTSkipUnless:try XCTSkipIf(condition) -> @Test(.disabled(if: condition))。
- 用测试或套件上的 traits 替换 XCTSkipIf/XCTSkipUnless:try XCTSkipUnless(condition) -> @Test(.enabled(if: condition))。
- 把测试中途的 throw XCTSkip("reason") 替换为 try Test.cancel("reason")。
- 当跳过检查的是 OS 版本或平台可用性时,用测试函数上的 @available 属性替代 .enabled(if:)。
- 用 withKnownIssue("...") { ... } 替换 XCTExpectFailure("...", ...) { ... }。
- 对于间歇性失败,把 .nonStrict() 选项(或简写 strict: false 参数)替换为 isIntermittent: true。
- 对于条件/匹配:使用 when: 和 matching: 参数,例如 withKnownIssue("...") { try riskyOperation() } when: { shouldExpectFailure } matching: { issue in issue.error != nil }。
- 并发与串行执行:XCTest 默认在主 actor 上运行同步测试,并在套件内顺序执行;Swift Testing 在任意任务上并行运行所有测试函数。
- 只有测试在 XCTest 形式中显式依赖主 actor 隔离时,才添加 @MainActor。
- 如果测试依赖共享状态,添加 @Suite(.serialized)。
- 用 Attachment.record(value) 替换 XCTAttachment + self.add(attachment)。
- 被附加类型必须遵循 Attachable;当导入 Foundation 时,Codable 和 NSSecureCoding 类型自动满足。
- 迁移 setUp 时,把隐式解包可选属性转换为非可选属性,并在原地初始化;如果初始化复杂、可能 throw 或 async,则在 init 中初始化。
- 查找显式 XCTFail/Issue.record 调用,尽量转换为 #expect 或 #require。
- 不要把 try #require 调用改成 #expect;这会改变测试行为。
- 只给显式依赖 XCTest 隐式主 actor 隔离的测试添加 @MainActor;不要不必要地添加。
- 查找对输入循环或许多逻辑相同的重复测试,转换为使用 @Test(arguments:) 的参数化测试。
- 对于测试间有共享可变状态的套件,添加 @Suite(.serialized),并考虑使用 actor 或 class 而不是 struct。
- 不要使用下划线前缀符号如 #_sourceLocation;只使用公开 API。对于 source location,始终使用完整的 SourceLocation(fileID:filePath:line:column:) 初始化器。
- 检查已有 @Test 函数的多词 camelCase 名称,并转换为句子式大小写的原始标识符。
- 原始标识符语法只用于读起来像句子的多词名称。
适用边界 · 注意事项
- 用户要求从零编写新测试(没有现成 XCTest 代码)、只询问 XCTest 功能、只询问测试结果或测试运行、要求更新测试以覆盖新功能而不是更新测试本身、或只是在调试测试失败且未提及迁移时,不适用。
- 用户使用 XCUI* API 的 UI 自动化测试时不适用,因为这些不能迁移到 Swift Testing。