Swift Testing 框架
使用 Swift Testing 框架进行现代测试。不使用 XCTest。
概述
Swift Testing 以基于宏的现代方式取代 XCTest,更简洁、对异步支持更好,并默认并行运行测试。核心原则:如果你学过 XCTest,请忘掉它——Swift Testing 的工作方式不同。
参考资料
Apple 文档
迁移指南
核心概念
断言
宏 | 用途
#expect(expression) | 软检查——失败后继续。用于大多数断言。
#require(expression) | 硬检查——失败即停止测试。仅用于前置条件。
可选值解包
let user = try #require(await fetchUser(id: "123")) #expect(user.id == "123")
测试结构
import Testing @testable import YourModule
@Suite struct FeatureTests { let sut: FeatureType
init() throws {
sut = FeatureType()
}
@Test("Description of behavior")
func testBehavior() {
#expect(sut.someProperty == expected)
}
}
断言转换
XCTest | Swift Testing
XCTAssert(expr) | #expect(expr)
XCTAssertEqual(a, b) | #expect(a == b)
XCTAssertNil(a) | #expect(a == nil)
XCTAssertNotNil(a) | #expect(a != nil)
try XCTUnwrap(a) | try #require(a)
XCTAssertThrowsError | #expect(throws: ErrorType.self) { }
XCTAssertNoThrow | #expect(throws: Never.self) { }
错误测试
#expect(throws: (any Error).self) { try riskyOperation() } #expect(throws: NetworkError.self) { try fetch() } #expect(throws: NetworkError.timeout) { try fetch() } #expect(throws: Never.self) { try safeOperation() }
参数化测试
@Test("Validates inputs", arguments: zip( ["a", "b", "c"], [1, 2, 3] )) func testInputs(input: String, expected: Int) { #expect(process(input) == expected) }
警告:多个集合不使用 zip 会生成笛卡尔积。
异步测试
@Test func testAsync() async throws { let result = try await fetchData() #expect(!result.isEmpty) }
确认(Confirmations)
@Test func testCallback() async { await confirmation("callback received") { confirm in let sut = SomeType { confirm() } sut.triggerCallback() } }
标签
extension Tag { @Tag static var fast: Self @Tag static var networking: Self }
@Test(.tags(.fast, .networking)) func testNetworkCall() { }
常见陷阱
过度使用 #require——大多数检查应使用 #expect
忘记状态隔离——每个测试获得一个新实例
意外产生笛卡尔积——配对输入始终使用 zip
未使用 .serialized——对线程不安全的遗留测试应使用
常见错误
过度使用 #require——#require 仅用于前置条件。将其用于普通断言会导致测试在第一次失败时停止,而不是报告所有失败。断言使用 #expect,仅当后续断言依赖该值时才使用 #require。
笛卡尔积错误——@Test(arguments: [a, b], [c, d]) 会生成 4 种组合,而不是 2 种。始终使用 zip 正确配对参数:arguments: zip([a, b], [c, d])。
忘记状态隔离——Swift Testing 为每个测试方法创建新的测试实例。但测试之间的共享状态(静态变量、单例)仍会泄漏。使用依赖注入或在测试之间清理单例。
并行测试冲突——Swift Testing 默认并行运行测试。涉及共享文件、数据库或单例的测试会相互干扰。使用 .serialized 或隔离策略。
未自然使用异步——将异步操作包装在 Task { } 中会违背初衷。直接在测试函数签名中使用 async/await:@Test func testAsync() async throws { }。
确认误用——confirmation 用于验证回调是否被调用。将其用于断言是错误的。断言使用 #expect,回调计数使用 confirmation。