关于 swift-testing
面向 Swift 开发者的现代单元测试指南,介绍 Swift Testing 的测试声明、断言、并行执行和 XCTest 迁移边界,并说明进阶 API 的版本要求。
主要功能
- 01使用 @Test 声明测试
- 02通过 #expect 与 #require 断言
- 03套件、标签与测试特征
- 04并行执行与状态隔离
- 05与 XCTest 共存及迁移
- 06版本门控 API 指引
适用场景
- 01为 Swift 代码编写行为测试
- 02测试异步操作与错误路径
- 03隔离访问共享外部状态的测试
- 04逐套件迁移 XCTest 单元测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swift-testing安装后在 Claude Code 中输入/swift-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/testing-advanced.mdtesting-patterns.md详细介绍
Swift Testing
Swift Testing 是 Swift 的现代化测试框架(Xcode 16+、Swift 6+)。新增单元测试时优先使用它。迁移尚未完成时保留 XCTest;UI 自动化、性能 API、Objective-C 异常测试和常见的快照测试工具则使用 XCTest。
目录
- 基础测试
@Test Traits- #expect 和 #require
@Suite 和测试组织- 执行模型
- XCTest 迁移边界
- 已知问题
- 其他模式
- 常见错误
- 测试附件
- 退出测试
- 受版本限制的 API
- 审查清单
- 参考资料
基础测试
import Testing
@Test("User can update their display name")
func updateDisplayName() {
var user = User(name: "Alice")
user.name = "Bob"
#expect(user.name == "Bob")
}
@Test 特征
@Test("Validates email format") // display name
@Test(.tags(.validation, .email)) // tags
@Test(.disabled("Server migration in progress")) // disabled
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // conditional
@Test(.bug("https://github.com/org/repo/issues/42")) // bug reference
@Test(.timeLimit(.minutes(1))) // time limit
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // combined
#expect 和 #require
// #expect records failure but continues execution
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")
// #expect with error type checking
#expect(throws: ValidationError.self) {
try validate(email: "not-an-email")
}
// #expect with specific error value
#expect {
try validate(email: "")
} throws: { error in
guard let err = error as? ValidationError else { return false }
return err == .empty
}
// #require records failure AND stops test (like XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")
// #require for optionals -- unwraps or fails
let first = try #require(items.first)
#expect(first.isValid)
规则:后续断言依赖某个值时,使用 #require。对于彼此独立的检查,使用 #expect。
@Suite 和测试组织
有关套件组织、确认模式、已知问题处理和执行模型的详情,请参阅 references/testing-patterns.md。
执行模型
Swift Testing 默认并行运行测试。除非明确设计为如此,否则不要假设测试顺序、套件实例共享,或对可变状态具有独占访问权。
@Suite(.serialized)
struct KeychainTests {
@Test func storesToken() throws { /* ... */ }
@Test func deletesToken() throws { /* ... */ }
}
当测试或套件因访问共享外部状态而必须逐个运行时,使用 .serialized。它不会让该范围之外的无关测试也按串行方式运行。
规则:
- 每项测试都必须自行设置状态。
- 共享可变全局变量属于缺陷,除非它们受到保护或明确采用串行方式运行。
@Suite(.serialized)用于独占执行,而不是表达测试之间的逻辑顺序。- 如果测试依赖执行顺序,请将它们合并为一项测试,或将执行序列移入共享辅助代码。
XCTest 迁移边界
Swift Testing 单元测试不会继承自 XCTestCase。在独立函数或 struct、class、actor 等套件类型的方法上声明 @Test;不需要实例 fixture 时,使用 static 或 class 方法。
迁移期间,XCTest 和 Swift Testing 可以共存。每次迁移一个文件或套件,并比较测试发现数、通过数、失败数和跳过数;UI 自动化、性能基准测试和常见快照测试流程则继续使用 XCTest/XCUITest 或快照工具。如果这样能让运行器预期更清晰,请将它们分隔到不同文件或目标中。
对于 Xcode 27 时代的混合辅助工具,应检查已配置的互操作模式,而不是声称跨框架 API 禁止使用。旧版测试计划继承 limited;新项目使用 complete;此外还有 strict 和 none。迁移期间优先使用 complete 或 strict;SwiftPM 需要时使用 SWIFT_TESTING_XCTEST_INTEROP_MODE。有关模式矩阵和工具链版本限制,请参阅 references/testing-advanced.md。
不要机械地将每个 XCTest 断言都替换为 #expect;请根据以下迁移默认规则,保留必要的解包和无条件失败:
XCTAssert*->#expect(...)XCTUnwrap或后续检查所需的任何值 ->try #require(...)XCTFail("...")或手动记录的无条件问题 ->Issue.record("...")- UI 测试、性能基准测试和常见快照测试流程继续使用 XCTest/XCUITest 或快照工具。
- 将
@available用于单个@Test函数,而不是套件类型或其外层类型。
迁移示例请参阅 references/testing-patterns.md;Swift/Xcode 版本限制请参阅 references/testing-advanced.md。
已知问题
标记预期失败,以免它们导致测试失败:
withKnownIssue("Propane tank is empty") {
#expect(truck.grill.isHeating)
}
// Intermittent / flaky failures
withKnownIssue(isIntermittent: true) {
#expect(service.isReachable)
}
// Conditional known issue
withKnownIssue {
#expect(foodTruck.grill.isHeating)
} when: {
!hasPropane
}
如果没有记录任何已知问题,Swift Testing 会记录一条独立问题,通知你该问题可能已经解决。
其他模式
参数化测试、标签和测试套件、异步测试、特征及执行模型的详细信息,请参阅 references/testing-patterns.md。
测试附件
将诊断数据附加到测试结果中,以便调试失败。完整示例请参阅 references/testing-patterns.md。
@Test func generateReport() async throws {
let report = try generateReport()
Attachment.record(report.data, named: "report.json")
#expect(report.isValid)
}
对于图像附件及其工具链门槛,请使用 版本门控 API 中的规范表格。
退出测试
在受支持的运行时上测试调用 exit()、fatalError() 或 preconditionFailure() 的代码。更正退出测试代码时,请注明 版本门控 API 中的确切门槛。
@Test func invalidInputCausesExit() async {
await #expect(processExitsWith: .failure) {
processInvalidInput() // calls fatalError()
}
}
版本门控 API
对于高级 API,请在更正旁注明确切的工具链和运行时门槛。这是规范摘要;详细矩阵和示例请参阅 references/testing-advanced.md。
@Test func exitsWithCapturedCode() async {
let expectedCode: Int32 = 42
await #expect(processExitsWith: .failure) { [expectedCode] in
exit(expectedCode)
}
}
| 要更正的用户代码 | 当前指导 |
|---|---|
#expect(exitsWith:) | 使用 await #expect(processExitsWith: .failure) { ... }。退出测试需要 Swift 6.2 / Xcode 26.0 或更高版本,且受 macOS、Linux、FreeBSD、OpenBSD 和 Windows 运行时目标支持,不支持 iOS、tvOS 或 watchOS。对于 iOS 应用目标,请通过较小的非退出 API 或受支持的宿主 / 工具目标测试致命错误路径逻辑。 |
| 退出测试闭包读取外部值 | 添加显式捕获列表,例如 { [expectedCode] in ... }。退出测试捕获列表需要 Swift 6.3 编译器;捕获的值必须符合 Sendable 和 Codable。 |
在等待任务的测试中使用 Test.cancel() | 将测试声明为 async throws,并调用 try Test.cancel("reason")。Test.cancel(_:) 需要 Swift 6.3 / Xcode 26.4 时代的支持。 |
Issue.record(..., severity: .warning) | 使用 Issue.record("message", severity: .warning)。警告严重级别会报告问题,但不会导致测试失败;此功能需要 Swift 6.3 / Xcode 26.4 时代的支持。 |
Attachment(image, named:).record() | 使用 Attachment.record(image, named: "name", as: .png)。导入 Testing 以及相关图像框架;Apple 平台的图像值包括 UIImage、CGImage、CIImage 和 NSImage。记录图像附件需要 Swift 6.3 / Xcode 26.4 时代的支持。 |
常见错误
- 测试实现,而非行为。 测试代码的实际行为,而不是实现方式。
- 没有错误路径测试。 如果函数可能抛出错误,就测试错误路径。
- 异步测试不稳定或使用休眠。 使用
confirmation、时钟注入或并发原语,而不是休眠。 - 测试之间共享可变状态。 每个测试都通过
@Suite中的init()设置自己的状态。 - UI 测试中缺少无障碍标识符。 XCUITest 查询依赖这些标识符。
- 未测试取消行为。 如果代码支持取消
Task,请确认它能干净地取消。 - 迁移边界不清晰。 遵循 XCTest 迁移边界,不要将任一框架视为只能全盘采用或完全弃用。
- 测试之间共享非
Sendable辅助项。 让共享辅助项符合Sendable;使用@MainActor标注依赖 MainActor 的测试代码。 - 将串行执行视为有序执行。 默认情况下,测试会并行运行;
.serialized可保护独占状态,但不会让一个测试为另一个测试提供输入。
审查清单
- 所有新增测试都使用 Swift Testing(
@Test、#expect),而非 XCTest 断言 - 测试名称描述行为(
fetchUserReturnsNilOnNetworkError,而非testFetchUser) - 错误路径有专门的测试
- 异步测试使用
confirmation(),而非Task.sleep - 对重复的变体使用参数化测试
- 应用标签以便筛选(
.critical、.slow) - 模拟对象遵循协议,而非继承具体类型
- 测试之间没有共享的可变状态
- 测试不依赖声明顺序或共享的套件实例
- 仅在状态确实互斥时使用
.serialized,而非将其用于模拟工作流顺序 - 对可取消的异步操作测试取消行为
参考资料
- 测试模式:references/testing-patterns.md
- 高级测试(警告、取消、图像附件):references/testing-advanced.md