关于 modernize-tests
用于现代化 Swift 测试的技能,可将符合条件的 XCTest 代码迁移到 Swift Testing,并更新现有测试的推荐写法。面向维护 Swift 测试套件的开发者,涵盖迁移细节及必须保留在 XCTest 中的情况。
主要功能
- 01将 XCTest 测试迁移到 Swift Testing
- 02将断言映射为期望表达式
- 03更新异步期望和跳过方式
- 04处理并发与共享状态
- 05识别不可迁移的测试
适用场景
- 01逐步迁移一个测试类
- 02更新旧式断言写法
- 03转换异步测试期望
- 04让 UI 和性能测试保留在 XCTest 中
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tartinerlabs/ skills --skill modernize-tests安装后在 Claude Code 中输入/modernize-tests确认已加载。
详细介绍
现代化测试
测试现代化涉及两项潜在操作:从 XCTest 迁移到 Swift Testing,以及更新现有 Swift Testing 测试以采用推荐模式。
在可能的情况下,应将 XCTest 迁移到 Swift Testing。不过,并非所有 XCTest 都能迁移到 Swift Testing。
- UI 测试(使用 XCUIAutomation 的测试)无法使用 Swift Testing 编写,必须保留为 XCTest。
- 使用
measure { ... }系列 API 进行性能测量的 XCTest 无法迁移。不过,XCTestCase 中不使用 XCTest 性能 API 的其他测试方法可以迁移。
迁移参考
导入
将 import XCTest 替换为 import Testing。在增量迁移期间,如果文件中包含混合测试内容,可以同时导入两者。
移除 import XCTest 时,请检查文件是否使用了 Foundation 类型(URL、CharacterSet、ProcessInfo、Data 等)。XCTest 会重新导出 Foundation,因此如有需要,请添加 import Foundation。
将测试类改为套件
移除对 XCTestCase 的继承。优先使用结构体而不是类:
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(因为结构体没有 deinit)。将存储属性改为不使用隐式解包可选类型,并将其初始赋值从 setUp 移至内联初始化,或在初始化较复杂时移至初始化器中。
struct MyTests {
var fixture = Fixture()
mutating func `Fixture behaves as expected`() {
#expect(fixture.doSomething())
}
}
避免将实例变量移入函数体;这样可能会造成冗余。Swift Testing 会在每次运行测试前重新调用初始化器。如果测试会修改具有值语义的实例变量,可能需要将测试函数标记为 mutating。
测试方法
将 test 名称前缀替换为 @Test 属性。如果生成的测试名称包含多个驼峰式单词,请使用原始标识符,并将测试名称写成句子形式。
func testEngineDoesNotStall() { ... }->@Test funcEngine does not stall() { ... }func testIgnition() { ... }->@Test func ignition() { ... }
测试函数可以是 async、throws 或 async throws,也可以使用 @MainActor 隔离到全局 Actor。
将断言替换为期望
将测试从 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:) 没有直接对应项;请直接使用浮点运算。
错误
当错误类型遵循 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,这意味着期望不会中止测试运行。
某些 XCTestCases 会设置 continueAfterFailure = false,这意味着 XCTAssert 系列函数
会抛出 Objective-C 异常,从而中止测试执行。
当测试方法设置 continueAfterFailure = false 时,后续所有断言都需要使用 try #require(x)
而不是 #expect(x),以保留此行为。添加 try #require(x) 时,请为受影响的方法添加 throws。
当在 setUp 中设置 continueAfterFailure = false 时,转换为 try #require(x) 必须应用于该类中
所有测试方法的所有断言。
将 Issue.record/XCTFail 转换为期望
在不会造成干扰的情况下,将 Issue.record 或 XCTFail 转换为 #expect 或 #require,
具体取决于测试是否会在之后退出(同时考虑 continueAfterFailure)。
在某些情况下,期望本身的来源已足以说明失败原因,因此注释会显得多余。
例如,应按如下方式转换以下结构:
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())
将异步期望转换为确认
将 XCTestExpectation + fulfill() + await fulfillment(of:) 替换为 confirmation():
// Before
let exp = expectation(description: "...")
handler = { exp.fulfill() }
doWork()
await fulfillment(of: [exp])
// After
await confirmation("...") { confirm in
handler = { confirm() }
doWork()
}
对于 assertForOverFulfill = false 且设置了 expectedFulfillmentCount 的情况,请使用范围:
await confirmation("...", expectedCount: 10...) { confirm in ... }
跳过测试
将 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:)。
已知问题
将 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 测试。
- 除非需要
deinit(tearDown),否则优先使用struct编写测试套件;如果需要,则使用actor或final class。 - 添加
@Test时,从方法名中移除test前缀。对于较长且读起来像句子的测试名称,使用原始标识符语法以提升可读性,例如@Test funcAuthenticate, fetch summary, then check count() { ... }。 - 仅对读起来像句子的多词名称使用原始标识符语法。
- 迁移
setUp时,将隐式解包可选属性转换为非可选属性,并在原处初始化;如果初始化较复杂、可能抛出错误或是异步的,则在init中初始化。 - 查找可以转换为
#expect或#require的显式XCTFail/Issue.record调用。 - 不要将
try #require调用改为#expect;这会改变测试行为。 - 仅对明确依赖 XCTest 隐式主执行器隔离的测试添加
@MainActor。不要不必要地添加。 - 查找遍历输入的测试,或许多使用相同逻辑的重复测试,并将其转换为使用
@Test(arguments:)的参数化测试。 - 对测试之间共享可变状态的测试套件,添加
@Suite(.serialized),并考虑使用actor或class,而不是struct。 - 不要引入下划线前缀符号(如
#_sourceLocation);只使用公共 API。 对于源代码位置,始终使用完整的SourceLocation(fileID:filePath:line:column:)初始化器。 - 如果测试套件已经使用
#_sourceLocation,不要在现代化过程中替换现有用法。 - 如果现代化任务较复杂,必要时将工作分配给多个 Agent