SuperAPI AI 工具集

test-modernizer

测试与质量

查看项目

更新于 10-15 08:30

技能介绍

Test Modernizer 是一个面向 Swift 测试代码的现代化迁移技能,核心目标是将 XCTest 测试套件迁移到 Swift Testing,并对已有的 Swift Testing 测试进行评估和结构优化,以采用更新的特性。

适用场景 当用户要求现代化、更新、迁移、增强或转换测试时,该技能适用。具体来说,XCTest 应尽可能迁移到 Swift Testing;已有的 Swift Testing 测试应被评估,看是否可以通过采用新特性获得更好的结构。

不适用场景 该技能不适用于以下情况:用户要求从零编写新测试(没有现有 XCTest 代码);用户仅询问 XCTest 特性;用户仅询问测试结果或测试运行;用户要求更新测试以覆盖新功能,而不是更新测试本身;用户调试测试失败但未提及迁移;用户拥有使用 XCUI* API 的 UI 自动化测试(这些无法迁移到 Swift Testing)。

迁移参考 导入:将 import XCTest 替换为 import Testing。在增量迁移期间,如果文件包含混合测试内容,可以同时导入两者。移除 import XCTest 时,需检查文件是否使用了 Foundation 类型(URL、CharacterSet、ProcessInfo、Data 等)。由于 XCTest 会重新导出 Foundation,如有需要应添加 import Foundation。

测试类到套件:移除 XCTestCase 继承,优先使用 struct 而非 class,例如 final class FoodTruckTests: XCTestCase { ... } 改为 struct FoodTruckTests { ... }。

setUp/tearDown 到 init/deinit:将 override func setUp() 替换为 init()(可以是 async throws)。将 override func tearDown() 替换为 deinit。如果需要 deinit,应使用 actor 或 final class 而非 struct(因为 struct 没有 deinit)。将存储属性改为不使用隐式解包可选类型,并将其初始赋值从 setUp 移到内联初始化,或者如果初始化复杂,则移到初始化器中。

避免将实例变量拉入函数体,这会造成噪音。Swift Testing 在每次测试运行前会重新调用初始化器。如果测试使用值语义修改实例变量,可能需要将测试函数标记为 mutating。

测试方法:将 test 名称前缀替换为 @Test 属性。如果生成的测试名称包含多个驼峰词,使用原始标识符,测试名称为句子大小写。例如 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() } // Check error;XCTAssertNoThrow(try f()) 改为 #expect(throws: Never.self) { try f() }。

continueAfterFailure:默认 continueAfterFailure 为 true,意味着期望不会停止测试运行。某些 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 提升为期望:在不造成干扰的情况下,将 Issue.record 或 XCTFail 的使用转换为 #expect 或 #require,取决于测试是否随后退出(考虑 continueAfterFailure)。在某些情况下,期望本身的来源足以解释失败,注释将是多余的。例如,guard let object = somethingOptional() else { Issue.record("Could not get object"); return } 改为 let object = try #require(somethingOptional(), "Could not get object");guard object.isAvailable() else { Issue.record("Object not available"); return } 改为 try #require(object.isAvailable());if !object.performOperation() { Issue.record("Failed to perform operation") } 改为 #expect(object.performOperation())。

异步期望到确认:将 XCTestExpectation + fulfill() + await fulfillment(of:) 替换为 confirmation()。例如,之前: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 ... }。

跳过测试:将 XCTSkipIf/XCTSkipUnless 替换为测试或套件上的 trait:try XCTSkipIf(condition) 改为 @Test(.disabled(if: condition));try XCTSkipUnless(condition) 改为 @Test(.enabled(if: condition))。将测试中途的 throw XCTSkip("reason") 替换为 try Test.cancel("reason")。当跳过检查操作系统版本或平台可用性时,将其替换为测试函数上的 @available 属性,而不是 .enabled(if:)。

已知问题:将 XCTExpectFailure("...", ...) { ... } 替换为 withKnownIssue("...") { ... }。对于间歇性失败,将 .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)。

附件:将 XCTAttachment + self.add(attachment) 替换为 Attachment.record(value)。附加类型必须符合 Attachable(当导入 Foundation 时,Codable 和 NSSecureCoding 类型自动符合)。

现代化指南 从 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 隐式主 actor 隔离的测试添加 @MainActor。不要不必要地添加它。

查找循环遍历输入或许多具有相同逻辑的重复测试,并使用 @Test(arguments:) 将其转换为参数化测试。

对于测试之间共享可变状态的套件,添加 @Suite(.serialized),并考虑使用 actor 或 class 而非 struct。

不要使用下划线前缀的符号,如 #_sourceLocation;只使用公共 API。对于源位置,始终使用完整的 SourceLocation(fileID:filePath:line:column:) 初始化器。