Highlight
Swift Testing 的
#expect(throws:)宏用一行代码替代 do-catch 模板,withKnownIssue标记预期失败不污染报告,参数化测试让一个测试函数覆盖多组输入——这三个功能是本 session 的核心增量。
核心内容
写测试时,错误处理是最容易被跳过的部分。用 XCTest 测试一个抛出错误的函数,你需要手写 do-catch,代码冗长,而且如果函数没抛错,do-catch 不会提醒你”测试其实没生效”。Swift Testing 的 #expect(throws:) 宏直接解决这个问题:传一个错误类型进去,函数抛了对应类型的错误就通过,没抛就失败,一行代码搞定。
另一个常见痛点是”已知失败的测试”。某个测试因为外部依赖坏掉了,你暂时修不了,但它每次都在报告里标红,淹没了真正需要关注的失败。有人用 .disabled 跳过整个测试,但这样连编译错误也看不到了。withKnownIssue 是更精确的工具:测试照常运行,如果失败了标记为”预期失败”而不计入失败总数;如果哪天修好了,它会提醒你移除 withKnownIssue。
参数化测试解决的是重复代码的问题。一个枚举有六个 case,你要测每个 case 的 containsNuts 属性。复制粘贴六个测试函数?容易出错。for 循环遍历?一个失败会中断后续测试,而且报错信息不告诉你具体哪个值出问题。@Test(arguments:) 让你把输入数组提到测试属性上,框架自动为每个参数生成独立的测试用例,失败时清晰标注哪个输入有问题,且所有用例并行运行。
详细内容
错误断言:#expect(throws:)
#expect(throws:) 提供四个层级的精度(02:57):
// 只关心"有没有抛错"
#expect(throws: (any Error).self) {
try teaLeaves.brew(forMinutes: 200)
}
// 关心"抛的是不是这个类型"
#expect(throws: BrewingError.self) {
try teaLeaves.brew(forMinutes: 200)
}
// 关心"是不是这个具体的 case"
#expect(throws: BrewingError.oversteeped) {
try teaLeaves.brew(forMinutes: 200)
}
// 自定义验证逻辑(检查关联值等)
#expect {
try teaLeaves.brew(forMinutes: 3)
} throws: { error in
guard let error = error as? BrewingError,
case let .needsMoreTime(optimalBrewTime) = error else {
return false
}
return optimalBrewTime == 4
}
关键点:
throws: (any Error).self— 只要抛了任何错误就算通过,适合不关心具体错误类型的场景throws: BrewingError.self— 用类型过滤,抛了别的类型的错误会失败throws: BrewingError.oversteeped— 精确匹配某个 case,包括关联值- 闭包形式 — 最灵活,可以写任意判断逻辑,闭包返回
Bool决定测试是否通过
#require(throws:) 是它的”必须通过”变体(04:11):如果断言失败,测试立即终止,后续代码不再执行。适合验证关键前置条件。
已知问题:withKnownIssue
@Test func softServeIceCreamInCone() throws {
let iceCreamBatter = IceCreamBatter(flavor: .chocolate)
try #require(iceCreamBatter != nil)
#expect(iceCreamBatter.flavor == .chocolate)
withKnownIssue {
try softServeMachine.makeSoftServe(in: .cone)
}
}
关键点(04:29):
withKnownIssue只包裹会失败的部分,其他断言照常运行- 失败被记录为”预期失败”而非”测试失败”,不污染测试报告
- 当底层问题修复、代码不再抛错时,框架会提醒你移除
withKnownIssue - 比
.disabled更好:测试仍然编译和运行,不会漏掉编译错误
参数化测试
@Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip])
func doesNotContainNuts(flavor: IceCream.Flavor) throws {
try #require(!flavor.containsNuts)
}
@Test(arguments: [IceCream.Flavor.rockyRoad, .pistachio])
func containNuts(flavor: IceCream.Flavor) {
#expect(flavor.containsNuts)
}
关键点(08:44):
@Test(arguments:)接受任何 Sendable 集合(数组、字典、Range 等)- 每个参数生成独立的测试用例,失败时精确标注哪个输入有问题
- 所有用例并行运行,比 for 循环更快
- 输入类型遵循
Codable时,Xcode 支持单独重跑某个失败的用例
多参数组合(11:07):
@Test(arguments: Ingredient.allCases, Dish.allCases)
func cook(_ ingredient: Ingredient, into dish: Dish) async throws { ... }
两个集合做笛卡尔积,4 个食材 x 4 个菜品 = 16 个用例。如果不想测所有组合,用 zip 配对:
@Test(arguments: zip(Ingredient.allCases, Dish.allCases))
func cook(_ ingredient: Ingredient, into dish: Dish) async throws { ... }
zip 把两个集合按顺序配对,只生成 4 个用例。
Suite、Tag 和并行测试
@Suite("Various desserts")
struct DessertTests {
@Suite struct WarmDesserts {
@Test func applePieCrustLayers() { /* ... */ }
@Test func lavaCakeBakingTime() { /* ... */ }
}
@Suite struct ColdDesserts {
@Test func cheesecakeBakingStrategy() { /* ... */ }
@Test func mangoSagoToppings() { /* ... */ }
}
}
Suite 支持嵌套(13:11),用于按逻辑分组测试函数。
Tag 用于跨 Suite 关联测试(13:36):
extension Tag {
@Tag static var caffeinated: Self
@Tag static var chocolatey: Self
}
@Suite(.tags(.caffeinated)) struct DrinkTests { ... }
@Test(.tags(.caffeinated, .chocolatey)) func espressoBrownieTexture() { ... }
Tag 出现在 Xcode 16 的测试导航器和测试报告中。你可以在测试计划里按 Tag 筛选包含/排除的测试,测试报告的”分布洞察”功能会自动发现具有相同 Tag 的失败测试的共性。
并行测试默认开启(21:20)。测试运行顺序随机,帮助暴露隐含的数据依赖。如果暂时无法解决依赖,用 .serialized trait 让 Suite 内的测试串行执行:
@Suite("Cupcake tests", .serialized)
struct CupcakeTests { ... }
.serialized 会自动继承到嵌套的子 Suite,不需要重复添加。
异步回调测试:confirmation
对于可能触发多次的回调,用 confirmation 替代手动计数(25:50):
@Test func bakeCookies() async throws {
let cookies = await Cookie.bake(count: 10)
try await confirmation("Ate cookies", expectedCount: 10) { ateCookie in
try await eat(cookies, with: .milk) { cookie, crumbs in
#expect(!crumbs.in(.milk))
ateCookie()
}
}
}
expectedCount 指定期望的回调次数。如果实际调用次数不匹配,测试失败。传 0 可以断言某个回调永远不应该触发。
核心启发
-
用
#expect(throws:)替换所有 do-catch 测试模式:为什么值得做:do-catch 写法冗长且容易遗漏”没抛错”的情况,#expect(throws:)一行搞定,语义更清晰。怎么开始:找到项目中所有测试错误抛出的 do-catch 块,逐个替换为#expect(throws: ErrorType.self)形式。 -
用参数化测试消灭重复的测试函数:为什么值得做:同一个逻辑测多组输入,复制粘贴容易出错且维护成本高,参数化测试让输入与逻辑分离,新增 case 只需在数组里加一行。怎么开始:识别项目中”只有输入值不同、断言逻辑相同”的测试函数,用
@Test(arguments:)合并。 -
用
withKnownIssue管理”暂时修不了”的失败测试:为什么值得做:.disabled会完全跳过测试,连编译错误都发现不了;withKnownIssue让测试照常运行,失败不计入总数,修好后自动提醒你移除。怎么开始:在 CI 报告中找到反复失败但你暂时不打算修的测试,用withKnownIssue包裹失败部分。 -
用 Tag 跨文件关联测试,配合测试计划做精准筛选:为什么值得做:项目变大后,按目录组织测试不够用;Tag 可以按业务属性(如”付费功能”、“网络依赖”)跨 Suite 聚合测试,在测试计划里排除不稳定的 Tag。怎么开始:给关键业务维度的测试加 Tag,在测试计划中用 include/exclude 字段按 Tag 筛选。
关联 Session
- Meet Swift Testing — Swift Testing 入门,介绍 @Test、#expect、Suite 等基础概念
- Consume noncopyable types in Swift — Swift 6 中 noncopyable 类型与测试的交互
- Explore Swift performance — Swift 性能调优,理解并行测试对性能的影响
- Explore the Swift on Server ecosystem — 服务器端 Swift 中测试框架的实践
评论
GitHub Issues · utterances