mcpField
M
modernize-tests

modernize-tests

github.com/tartinerlabs测试与质量8278 次安装6 天前更新
代码仓库

关于 modernize-tests

用于现代化 Swift 测试的技能,可将符合条件的 XCTest 代码迁移到 Swift Testing,并更新现有测试的推荐写法。面向维护 Swift 测试套件的开发者,涵盖迁移细节及必须保留在 XCTest 中的情况。

主要功能

  1. 01将 XCTest 测试迁移到 Swift Testing
  2. 02将断言映射为期望表达式
  3. 03更新异步期望和跳过方式
  4. 04处理并发与共享状态
  5. 05识别不可迁移的测试

适用场景

  1. 01逐步迁移一个测试类
  2. 02更新旧式断言写法
  3. 03转换异步测试期望
  4. 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 func Engine 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 func Authenticate, 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

评论

支持 Markdown