SuperAPI AI 工具集

test-modernizer

测试与质量

查看项目

更新于 10-15 10:54

技能介绍

Test Modernizer 是一个面向 Swift 测试代码的技能,核心用途是把 XCTest 测试迁移到 Swift Testing,或对已有的 Swift Testing 测试进行评估,看看是否能采用更新的特性进行更好的组织。

适用场景:用户要求对测试进行现代化、更新、迁移、增强或转换。迁移时,XCTest 应尽可能迁移到 Swift Testing;现有 Swift Testing 测试应评估是否能通过新特性获得更好的结构。

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

迁移参考要点如下。

导入:用 import Testing 替换 import XCTest。增量迁移期间,同一文件可同时导入两者。移除 import XCTest 时,需检查文件是否使用 Foundation 类型(URL、CharacterSet、ProcessInfo、Data 等);因为 XCTest 会重新导出 Foundation,必要时需补充 import Foundation。

测试类改为 Suite:移除 XCTestCase 继承,优先使用 struct 而非 class,例如 final class FoodTruckTests: XCTestCase 改为 struct FoodTruckTests。setUp/tearDown 对应改为 init/deinit:用 init()(可为 async throws)替换 override func setUp(),用 deinit 替换 override func tearDown()。若需要 deinit,则使用 actor 或 final class 而非 struct(struct 没有 deinit)。存储属性应改为非隐式解包可选类型,并把初始化赋值从 setUp 移到内联初始化,或在初始化复杂时放入初始化器。Swift Testing 会在每个测试运行前重新调用初始化器;若测试以值语义修改实例变量,可能需要把测试函数标记为 mutating。

测试方法:用 @Test 属性替换 test 名称前缀。若结果测试名包含多个 camelCase 单词,则使用原始标识符(raw identifier)写成句子式命名。测试函数可为 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() };若在闭包中检查错误,可改为 #expect(throws: specificError) { try f() };若需要拿到错误对象再检查,可写 let error = #expect(throws: (any Error).self) { try f() }。XCTAssertNoThrow(try f()) 对应 #expect(throws: Never.self) { try f() }。

continueAfterFailure:默认值为 true,即期望不会中断测试运行。某些 XCTestCase 会设置 continueAfterFailure = false,使 XCTAssert 系列抛出 Objective-C 异常并中断测试执行。当测试方法设置 continueAfterFailure = false 时,后续所有断言都需要使用 try #require(x) 而非 #expect(x),以保留该行为;添加 try #require(x) 时需给受影响方法添加 throws。若 continueAfterFailure = false 在 setUp 中设置,则转换需应用到该类所有测试方法中的所有断言。

把 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())。

异步期望改为 Confirmation:用 confirmation() 替换 XCTestExpectation + fulfill() + await fulfillment(of:)。对于 assertForOverFulfill = false 且带 expectedFulfillmentCount 的情况,使用范围,例如 await confirmation("...", expectedCount: 10...) { confirm in ... }。

跳过测试:用测试或 suite 上的 trait 替换 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):用 withKnownIssue("...") { ... } 替换 XCTExpectFailure("...", ...) { ... }。对于间歇性失败,用 isIntermittent: true 替换 .nonStrict() 选项(或简写参数 strict: false)。条件/匹配时使用 when: 和 matching: 参数。

并发与串行执行:XCTest 默认在主 actor 上运行同步测试,并在 suite 内按顺序执行;Swift Testing 则在任意 task 上并行运行所有测试函数。仅当测试在 XCTest 形式下明确依赖主 actor 隔离时才添加 @MainActor;若测试依赖共享状态,则添加 @Suite(.serialized)。

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

现代化指南:从 XCTest 迁移时,一次迁移一个测试类。迁移期间一个文件可以同时包含 XCTest 和 Swift Testing 测试。除非需要 deinit(tearDown),否则 suite 优先使用 struct;若需要则使用 actor 或 final class。添加 @Test 时移除方法名中的 test 前缀。对于读起来像句子的较长测试名,使用原始标识符语法提高可读性,例如 @Test func Authenticate, fetch summary, then check count()。同时检查已有 @Test 函数中的多词 camelCase 名称,将其转换为句子式原始标识符。仅对读起来像句子的多词名称使用原始标识符语法。迁移 setUp 时,把隐式解包可选属性转换为就地初始化的非可选属性,或在初始化复杂、可能抛出或为异步时放入 init。查找可以转换为 #expect 或 #require 的显式 XCTFail/Issue.record 调用。不要把 try #require 调用改成 #expect,这会改变测试行为。仅对明确依赖 XCTest 隐式主 actor 隔离的测试添加 @MainActor,不要不必要地添加。查找循环遍历输入或多次重复相同逻辑的测试,将其转换为使用 @Test(arguments:) 的参数化测试。对于测试间共享可变状态的 suite,添加 @Suite(.serialized),并考虑使用 actor 或 class 而非 struct。不要使用下划线前缀符号如 #_sourceLocation,只使用公开 API;源位置始终使用完整的 SourceLocation(fileID:filePath:line:column:) 初始化器。