
WWDC 2024 - Understanding Swift Testing
WWDC 2024 - Understanding Swift Testing
This content is automatically translated from Chinese by AI. While we strive for accuracy, there might be nuances that are lost in translation.
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)
}
The @Test
macro is used to mark this function as a test;
The default access modifier is internal, with the scope being the entire module. @testable
essentially lifts this restriction, allowing tests to access it.
The #expect
macro is used to test whether a condition is true.
The #require
macro is used for assertions. If the condition is false, it will throw an exception and immediately stop the test.
try #require(session.isValid)
session.invalidate() // not executed
It can also be used to assert optional types, exiting the test if the value is nil.
let method = try #require(paymentMethods.first)
#expect(method.isDefault) // not executed
Traits
-
Display test descriptions
-
Customize when and whether test cases run
-
Modify the behavior of test case execution
Some built-in Traits, more details can be found in the documentation
Test Suite
Related test functions can be grouped together using the @Suite
macro. It can have instance variables, and you can use init
and deinit
to add initialization and cleanup logic.
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")
}
}
The @Suite
instance initializes a new instance for each @Test
method to prevent state sharing. Therefore, the above code can be rewritten as:
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 are used to control when tests run.
.enabled(if: ...)
, skips the test if the condition is false.
@Test(.enabled(if: AppFeatures.isCommentingEnabled)) // Test 'videoCommenting()' skipped
func videoCommenting() {
// ...
}
.disabled(...)
, can describe why the test is disabled.
@Test(.disabled("Due to a known crash"))
func example() {
// ...
}
.bug(...)
, can be used to associate an issue.
@Test(.disabled("Due to a known crash"), .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
// ...
}
You can use .available(...)
instead of #available(...)
to conditionally run tests based on OS version availability.
// Not recommend
@Test func hasRuntimeVersionCheck() {
guard #available(macOS 15, *) else { return }
}
@Test
@available(macOS 15, *)
func usesNewAPIs() {
// ...
}
Use .tags(...)
to add tags to tests for grouping, making it easier to view them by tag in Xcode.
Parameterized testing with 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
Comparison between XCTest and Swift Testing:
For more information, see Migrating a test from XCTest.
Open Source
Cross-platform support, SPM command line, Xcode, VSCode Swift Extension.
Command line: swift test
Additional Information
-
Original video: https://developer.apple.com/wwdc24/10179
-
Related: Further optimizing tests with Swift Testing