关于 test-modernizer
test-modernizer 是用于更新现有 Swift 测试的技能,重点支持从 XCTest 迁移到 Swift Testing。它提供测试结构、断言、并发、跳过逻辑和附件的转换指导,并指出不适合迁移的情况。
主要功能
- 01从 XCTest 迁移到 Swift Testing
- 02转换测试结构与初始化逻辑
- 03替换断言和错误检查
- 04处理并行执行与共享状态
- 05转换跳过逻辑和附件
适用场景
- 01迁移现有 XCTest 测试
- 02改进 Swift Testing 测试结构
- 03将重复测试改为参数化测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ superagents-lab/ xcode27-skills --skill test-modernizer安装后在 Claude Code 中输入/test-modernizer确认已加载。
详细介绍
测试现代化
适用情况:用户要求对测试进行现代化、更新、迁移、增强或转换。 在可能的情况下,应将 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(因为结构体没有 deinit)。将存储属性改为非隐式解包可选类型,并将其初始赋值从 setUp 移到内联初始化处;如果初始化较复杂,则移到初始化器中。
struct MyTests {
var fixture = Fixture()
mutating func `Fixture behaves as expected`() {
#expect(fixture.doSomething())
}
}
避免将实例变量移入函数体,这可能会造成冗余。Swift Testing 会在每次测试运行前重新调用初始化器。如果测试会修改具有值语义的实例变量,可能需要将测试函数标记为 mutating。
测试方法
将 test 名称前缀替换为 @Test 属性。如果生成的测试名称包含多个驼峰式单词,请使用原始标识符,并将测试名称写成句子形式。
func testEngineDoesNotStall() { ... }->@Test func发动机不会熄火() { ... }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() { ... }。 - 同时检查现有的
@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:)初始化器。