
WWDC 2024 - 了解 Swift Testing
WWDC 2024 - 了解 Swift Testing
Building Block
import Testing
@testable import DestinationVideo
@Test("Check video metadata") func videoMetaData() {
let video = Video(fileName: "By the Lake.mov")
let expectedMetadata = Metadata(duration: .seconds(90))
#expect(video.metadata == expectedMetadata)
}
@Test
宏用来标记这个函数是用来测试的;
默认访问修饰符是 internal,范围为整个 module,@testable
相当于可以解除这个禁制,让测试访问到。
#expect
宏用来测试条件是否为真
#require
宏用来断言,如果条件为假,会直接抛出异常,立即停止测试
try #require(session.isValid)
session.invalidate() // not executed
也可以用来断言可选类型,当空时退出测试
let method = try #require(paymentMethods.first)
#expect(method.isDefault) // not executed
Traits
-
展示测试描述
-
定制测试用例什么时候跑,是否跑
-
修改测试用例执行的行为
一些内置的 Traits,更多详见文档
Test Suite
可以将关联的测试函数放在一起,使用 @Suite 宏,可以有实例变量,也可以使用 init 和 deinit 来添加初始化与销毁的一些逻辑
struct VideoTests {
@Test("Check video metadata") func videoMetadata() {
let video = Video(fileName: "By the Lake.mov")
let expectedMetadata = Metadata(duration: .seconds(90))
#expect(video.metadata == expectedMetadata)
}
@Test func rating() async throws {
let video = Video(fileName: "By the Lake.mov")
#expect(video.contentRating == "G")
}
}
@Suite
实例为每一个 @Test
方法都初始化一个新的防止状态共享。所以上面的代码可以改写成:
struct VideoTests {
let video = Video(fileName: "By the Lake.mov")
@Test("Check video metadata") func videoMetadata() {
let expectedMetadata = Metadata(duration: .seconds(90))
#expect(video.metadata == expectedMetadata)
}
@Test func rating() async throws {
#expect(video.contentRating == "G")
}
}
Common workflows
运行时条件 Runtime conditions,用来控制测试什么时候跑。
.enabled(if: ...)
,当条件为 false 时会跳过测试
@Test(.enabled(if: AppFeatures.isCommentingEnabled)) // Test 'videoCommenting()' skipped
func videoCommenting() {
// ...
}
.disabled(...)
,可以描述为什么这个测试被禁用
@Test(.disabled("Due to a known crash"))
func example() {
// ...
}
.bug(...)
,可以用来关联一个 issue
@Test(.disabled("Due to a known crash"), .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
// ...
}
可以基于操作系统版本可用性来作为条件,使用 .available(...)
而不是 #available(...)
// Not recommend
@Test func hasRuntimeVersionCheck() {
guard #available(macOS 15, *) else { return }
}
@Test
@available(macOS 15, *)
func usesNewAPIs() {
// ...
}
使用 .tags(...)
来给测试添加标签,用来分组,方便在 Xcode 中根据标签分组查看
带参数的测试 parameterized testing,使用 arguments
// Before
struct VideoContinentsTests {
@Test func mentionsFor_A_Beach() async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: "A Beach"))
#expect(!video.mentionedContinents.isEmpty)
#expect(video.mentionedContinents.count <= 3)
}
@Test func mentionsFor_By_the_Lake() async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: "By the Lake"))
#expect(!video.mentionedContinents.isEmpty)
#expect(video.mentionedContinents.count <= 3)
}
@Test func mentionsFor_Camping_in_the_Woods() async throws {
let videoLibrary = try await VideoLibrary()
let video = try #require(await videoLibrary.video(named: "Camping in the Woods"))
#expect(!video.mentionedContinents.isEmpty)
#expect(video.mentionedContinents.count <= 3)
}
// ...and more, similar test functions
}
// After
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 and XCTest
XCTest 与 Swift Testing 的对比:
更多信息可以查看Migrating a test from XCTest
Open Source
全平台支持,SPM 命令行、Xcode、VSCode Swift Extension
Command line: swift test
其他信息
-
其他相关:利用 Swift Testing 进一步优化测试