概述
从 WWDC 24 开始,苹果推出了全新的测试机制:Swift Testing。利用它我们可以大幅度简化之前“老态龙钟”的 XCTest 编码范式,并且使得单元测试更加灵动自由,更符合 Swift 语言的优雅品味。
在这里我们会和大家一起初涉并领略 Swift Testing 的测试之美。
在本篇博文中,您将学到如下内容:
- 概述
- 3. @Test 宏
- 3.1 按条件启用和禁用测试
- 3.2 使用 Tag 逻辑分组测试方法
- 3.3 使用 arguments 避免重复测试
- 总结
测试为先,质量为王!无测试,不软件!
那还等什么呢?Let’s testing!!!😉
3. @Test 宏
从上篇的示例代码中,我们应该可以体会到新 Swift Testing 的些许妙处。接下来我们着重谈谈其中几个常用的宏。
首先是 @Test 宏,我们可以使用它将任意方法变为可测试方法。我们还可以在类型(结构或类)内部使用它们以便将相关的测试“聚散成组”:
struct ModelTests {
var model = Model.shared
@Test("测试创建 Itmes")
mutating func createItems() {
model.createItems()
#expect(!model.items.isEmpty, "应该成功创建若干 Items!")
}
@Test("测试删除所有 Item")
mutating func delItem(){
model.createItems()
#expect(!model.items.isEmpty)
model.deleteAllItems()
#expect(model.items.isEmpty, "应该成功删除所有 Items!")
}
}
在上面的代码中,我们将两个测试方法放到 ModelTests 结构里,从左边侧边栏的内容可以看到,这使得它们形成了一个新的“测试组” ModelTests:
这样做的好处主要有两个:
- 将测试方法分门别类,便于管理;
- 可以将测试的构造和析构操作统一放到类型对象的析构和析构方法中去,避免冗余;
除此之外,利用 @Test 宏的不同重载形式,我们还可以实现一些有趣的测试特性。
3.1 按条件启用和禁用测试
我们可以利用 enabled 这个@Test ConditionTrait 来实现按指定条件开启和禁止对应的测试方法。
如下代码所示,我们仅在 testSettings.needTestDeleting 属性值为 true 的情况下才进行 Item 的删除测试:
class TestSettings {
static let shared = TestSettings()
var needTestDeleting = false
}
let testSettings = TestSettings.shared
struct ModelTests {
var model = Model.shared
@Test("测试创建 Itmes")
mutating func createItems() {...}
// 仅在 testSettings.needTestDeleting 为真时使能 delItem 测试
@Test("测试删除所有 Item", .enabled(if: testSettings.needTestDeleting))
mutating func delItem(){
model.createItems()
#expect(!model.items.isEmpty)
model.deleteAllItems()
#expect(model.items.isEmpty, "应该成功删除所有 Items!")
}
}
当对应条件值为假时,Xcode 会跳过对应的 delItem() 测试方法:
当然,如果我们希望的话还可以使用 disabled 条件彻底禁止某个测试方法:
@Test("测试删除所有 Item", .disabled("可能会导致系统挂起的找不到北"))
mutating func delItem(){...}
3.2 使用 Tag 逻辑分组测试方法
除了通过结构或类来“物理的”组合测试方法以外,我们还可以通过标签(Tag)以逻辑的方式安排类似的测试。
为了达到这一目的,我们首先必须实现一个遵守 CodingKey 协议的实体类型:
struct MyCoding: CodingKey {
var codingValue: String
init?(intValue: Int) {
codingValue = "\(intValue)"
}
init?(stringValue: String) {
codingValue = stringValue
}
var stringValue: String {
codingValue
}
var intValue: Int? {
Int(codingValue)
}
}
当然,我们也可以让现有类型(比如 String)遵守 CodingKey 协议来简化代码。
接着为 Tag 类型扩展我们自定义的标签:
extension Tag {
static let createItem = Tag(codingKey: MyCoding(stringValue: "CreateItem")!)!
}
最后,在对应的测试方法上设置指定的 Tag 即可:
@Test(.tags(.createItem))
func checkItemsCount() async throws {
var model = Model.shared
model.createItems()
#expect(model.items.count == 3)
}
struct ModelTests {
var model = Model.shared
@Test("测试创建 Itmes", .tags(.createItem))
mutating func createItems() {...}
@Test("测试删除所有 Item", .disabled("可能会导致系统挂起找不到北"))
mutating func delItem(){...}
}
现在,我们可以按标签(例如 createItem)来组织和集中测试海量 Testing 方法啦:
3.3 使用 arguments 避免重复测试
@Test 还有一个非常有用的特性:简化测试重复代码。在下面的实现中我们就使用 arguments 特性向 mentionedContinentCounts 方法传入了若干测试参数,从而仅用一个测试方法就搞定了所有相似的测试:
struct VideoContinentsTests {
@Test("Number of mentioned continents", arguments: [
"A Beach",
"By the Lake",
"Camping in the Woods",
"The Rolling Hills",
"Ocean Breeze",
"Patagonia Lake",
"Scotland Coast",
"China Paddy Field",
])
func mentionedContinentCounts(videoName: String) async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: videoName))
#expect(!video.mentionedContinents.isEmpty)
#expect(video.mentionedContinents.count <= 3)
}
}
限于篇幅,关于 Swift Testing 参数化测试(Refactor several similar tests into a parameterized @Test function)的介绍这里就不再展开说明了。
想要详细了解相关内容的小伙伴们请到之前列出的 WWDC 24 官方 Testing 开发视频中进一步观赏吧。
在下一篇博文中,我们将介绍 Swift Testing 另一个不可或缺的宏:#expect,不见不散!
想要系统学习 Swift 的小伙伴们,请来我的《Swift语言开发精讲》专栏逛一逛哦:
- 《Swift 语言开发精讲》
总结
在本篇博文中,我们讨论了 Swift Testing 测试中至关重要的宏:@Test,我们随后还分别介绍了它的 3 种重载形式,小伙伴们值得拥有。
感谢观赏,我们下一篇见!😎