Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2024 EssentialsSwiftDeveloper Tools

WWDC24 · 24 min · Essentials / Swift / Developer Tools

Meet Swift Testing

Introducing Swift Testing: a new package for testing your code using Swift. Explore the building blocks of its powerful new API, discover how it can be applied in common testing workflows, and learn how it relates to XCTest and open source Swift.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:00 — Introduction
  • 0:59 — Agenda
  • 1:20 — Building blocks
  • 1:58 — Building blocks: @Test functions
  • 3:07 — Building blocks: Expectations (#expect and #require)
  • 6:02 — Building blocks: Traits
  • 6:49 — Building blocks: @Suite types
  • 8:34 — Building blocks: Designed for Swift
  • 9:14 — Common workflows
  • 9:29 — Common workflows: Tests with conditions
  • 10:56 — Common workflows: Tests with common characteristics
  • 13:13 — Common workflows: Tests with different arguments
  • 17:35 — Swift Testing and XCTest
  • 21:52 — Open source
  • 23:29 — Wrap up

Code shown on screen · 22 snippets

Write your first @Test function swift · at 1:54 ↗
import Testing

@Test func videoMetadata() {
    // ...
}
Validate an expected condition using #expect swift · at 2:35 ↗
import Testing
@testable import DestinationVideo

@Test func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}
Fix a bug in the code being tested swift · at 4:24 ↗
// In `Video.init(...)`
self.metadata = Metadata(forContentsOfUrl: url)
Add a display name to a @Test function swift · at 6:06 ↗
@Test("Check video metadata") func videoMetadata() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}
Add a second @Test function swift · at 6:58 ↗
@Test func rating() async throws {
    let video = Video(fileName: "By the Lake.mov")
    #expect(video.contentRating == "G")
}
Organize @Test functions into a suite type swift · at 7:18 ↗
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")
    }

}
Factor a common value into a stored property in the suite swift · at 8:04 ↗
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")
    }

}
Specify a runtime condition trait for a @Test function swift · at 9:32 ↗
@Test(.enabled(if: AppFeatures.isCommentingEnabled))
func videoCommenting() {
    // ...
}
Unconditionally disable a @Test function swift · at 9:49 ↗
@Test(.disabled("Due to a known crash"))
func example() {
    // ...
}
Include a bug trait with a URL along with other traits swift · at 10:15 ↗
@Test(.disabled("Due to a known crash"),
      .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
    // ...
}
Conditionalize a test based on OS version swift · at 10:33 ↗
@Test
@available(macOS 15, *)
func usesNewAPIs() {
    // ...
}
Prefer @available on @Test function instead of #available within the function swift · at 10:42 ↗
// ❌ Avoid checking availability at runtime using #available
@Test func hasRuntimeVersionCheck() {
    guard #available(macOS 15, *) else { return }

    // ...
}

// ✅ Prefer @available attribute on test function
@Test
@available(macOS 15, *)
func usesNewAPIs() {
    // ...
}
Add a tag to a @Test function swift · at 11:22 ↗
@Test(.tags(.formatting)) func rating() async throws {
    #expect(video.contentRating == "G")
}
Add another data formatting-related test with the same tag swift · at 11:48 ↗
@Test(.tags(.formatting)) func formattedDuration() async throws {
    let videoLibrary = try await VideoLibrary()
    let video = try #require(await videoLibrary.video(named: "By the Lake"))
    #expect(video.formattedDuration == "0m 19s")
}
Group related tests into a sub-suite swift · at 11:56 ↗
struct MetadataPresentation {
    let video = Video(fileName: "By the Lake.mov")

    @Test(.tags(.formatting)) func rating() async throws {
        #expect(video.contentRating == "G")
    }

    @Test(.tags(.formatting)) func formattedDuration() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "By the Lake"))
        #expect(video.formattedDuration == "0m 19s")
    }
}
Move common tags trait to @Suite attribute, so the suite's @Test functions will inherit the tag swift · at 12:05 ↗
@Suite(.tags(.formatting))
struct MetadataPresentation {
    let video = Video(fileName: "By the Lake.mov")

    @Test func rating() async throws {
        #expect(video.contentRating == "G")
    }

    @Test func formattedDuration() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "By the Lake"))
        #expect(video.formattedDuration == "0m 19s")
    }
}
Example of some repetitive tests which can be consolidated into a parameterized @Test function swift · at 13:27 ↗
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
}
Refactor several similar tests into a parameterized @Test function swift · at 14:07 ↗
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)
    }

}
Example of using a for…in loop to repeat a test, instead of a parameterized @Test function (not recommended) swift · at 16:29 ↗
// Using a for…in loop to repeat a test (not recommended)
@Test func mentionedContinentCounts() async throws {
    let videoNames = [
        "A Beach",
        "By the Lake",
        "Camping in the Woods",
    ]

    let videoLibrary = try await VideoLibrary()
    for videoName in videoNames {
        let video = try #require(await videoLibrary.video(named: videoName))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }
}
Refactor a test using a for…in loop into a parameterized @Test function swift · at 17:15 ↗
@Test(arguments: [
    "A Beach",
    "By the Lake",
    "Camping in the Woods",
])
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)
}
A newly-created Swift package with two simple @Test functions swift · at 22:47 ↗
import Testing
@testable import MyLibrary

@Test func example() throws {
    #expect("abc" == "abc")
}

@Test func failingExample() throws {
    #expect(123 == 456)
}
Running all tests for the package from Terminal bash · at 22:56 ↗
> swift test

Resources