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

2024 SwiftDeveloper Tools

WWDC24 · 27 min · Swift / Developer Tools

Go further with Swift Testing

Learn how to write a sweet set of (test) suites using Swift Testing’s baked-in features. Discover how to take the building blocks further and use them to help expand tests to cover more scenarios, organize your tests across different suites, and optimize your tests to run in parallel.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 42 snippets

Successful throwing function swift · at 0:01 ↗
// Expecting errors

import Testing

@Test func brewTeaSuccessfully() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    let cupOfTea = try teaLeaves.brew(forMinutes: 3)
}
Validating a successful throwing function swift · at 0:02 ↗
import Testing

@Test func brewTeaSuccessfully() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    let cupOfTea = try teaLeaves.brew(forMinutes: 3)
    #expect(cupOfTea.quality == .perfect)
}
Validating an error is thrown with do-catch (not recommended) swift · at 0:03 ↗
import Testing

@Test func brewTeaError() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 3)

    do {
        try teaLeaves.brew(forMinutes: 100)
    } catch is BrewingError {
        // This is the code path we are expecting
    } catch {
        Issue.record("Unexpected Error")
    }
}
Validating a general error is thrown swift · at 0:04 ↗
import Testing

@Test func brewTeaError() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    #expect(throws: (any Error).self) {
        try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test!
    }
}
Validating a type of error swift · at 0:05 ↗
import Testing

@Test func brewTeaError() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    #expect(throws: BrewingError.self) {
        try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test!
    }
}
Validating a specific error swift · at 0:06 ↗
import Testing

@Test func brewTeaError() throws {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    #expect(throws: BrewingError.oversteeped) {
        try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test!
    }
}
Complicated validations swift · at 0:07 ↗
import Testing

@Test func brewTea() {
    let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4)
    #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
    }
}
Throwing expectation swift · at 0:08 ↗
import Testing

@Test func brewAllGreenTeas() {
  #expect(throws: BrewingError.self) {
    brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2)
  }
}
Required expectations swift · at 0:09 ↗
import Testing

@Test func brewAllGreenTeas() throws {
  try #require(throws: BrewingError.self) {
    brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2)
  }
}
Control flow of validating an optional value (not recommended) swift · at 0:10 ↗
import Testing

struct TeaLeaves {symbols
    let name: String
    let optimalBrewTime: Int

    func brew(forMinutes minutes: Int) throws -> Tea { ... }
}

@Test func brewTea() throws {
    let teaLeaves = TeaLeaves(name: "Sencha", optimalBrewTime: 2)
    let brewedTea = try teaLeaves.brew(forMinutes: 100)
    guard let color = brewedTea.color else {
        Issue.record("Tea color was not available!")
    }
    #expect(color == .green)
}
Failing test with a throwing function swift · at 0:11 ↗
import Testing

@Test func softServeIceCreamInCone() throws {
    try softServeMachine.makeSoftServe(in: .cone)
}
Disabling a test with a throwing function (not recommended) swift · at 0:12 ↗
import Testing

@Test(.disabled) func softServeIceCreamInCone() throws {
    try softServeMachine.makeSoftServe(in: .cone)
}
Wrapping a failing test in withKnownIssue swift · at 0:13 ↗
import Testing

@Test func softServeIceCreamInCone() throws {
    withKnownIssue {
        try softServeMachine.makeSoftServe(in: .cone)
    }
}
Wrap just the failing section in withKnownIssue swift · at 0:14 ↗
import Testing

@Test func softServeIceCreamInCone() throws {
    let iceCreamBatter = IceCreamBatter(flavor: .chocolate)
    try #require(iceCreamBatter != nil)
    #expect(iceCreamBatter.flavor == .chocolate)

    withKnownIssue {
        try softServeMachine.makeSoftServe(in: .cone)
    }
}
Simple enumerations swift · at 0:15 ↗
import Testing

enum SoftServe {
    case vanilla, chocolate, pineapple
}
Complex types swift · at 0:16 ↗
import Testing

struct SoftServe {
    let flavor: Flavor
    let container: Container
    let toppings: [Topping]
}

@Test(arguments: [
    SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]),
    SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]),
    SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream])
])
func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
Conforming to CustomTestStringConvertible swift · at 0:17 ↗
import Testing

struct SoftServe: CustomTestStringConvertible {
    let flavor: Flavor
    let container: Container
    let toppings: [Topping]

    var testDescription: String {
        "\(flavor) in a \(container)"
    }
}

@Test(arguments: [
    SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]),
    SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]),
    SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream])
])
func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
An enumeration with a computed property swift · at 0:18 ↗
extension IceCream {
    enum Flavor {
        case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio

        var containsNuts: Bool {
            switch self {
            case .rockyRoad, .pistachio:
                return true
            default:
                return false
            }
        }
    }
}
A test function for a specific case of an enumeration swift · at 0:19 ↗
import Testing

@Test func doesVanillaContainNuts() throws {
    try #require(!IceCream.Flavor.vanilla.containsNuts)
}
Separate test functions for all cases of an enumeration swift · at 0:20 ↗
import Testing

@Test func doesVanillaContainNuts() throws {
    try #require(!IceCream.Flavor.vanilla.containsNuts)
}

@Test func doesChocolateContainNuts() throws {
    try #require(!IceCream.Flavor.chocolate.containsNuts)
}

@Test func doesStrawberryContainNuts() throws {
    try #require(!IceCream.Flavor.strawberry.containsNuts)
}

@Test func doesMintChipContainNuts() throws {
    try #require(!IceCream.Flavor.mintChip.containsNuts)
}

@Test func doesRockyRoadContainNuts() throws {
    try #require(!IceCream.Flavor.rockyRoad.containsNuts)
}
Parameterizing a test with a for loop (not recommended) swift · at 0:21 ↗
import Testing

extension IceCream {
    enum Flavor {
        case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio
    }
}

@Test
func doesNotContainNuts() throws {
    for flavor in [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip] {
        try #require(!flavor.containsNuts)
    }
}
Swift testing parameterized tests swift · at 0:22 ↗
import Testing

extension IceCream {
    enum Flavor {
        case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio
    }
}

@Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip])
func doesNotContainNuts(flavor: IceCream.Flavor) throws {
    try #require(!flavor.containsNuts)
}
100% test coverage swift · at 0:23 ↗
import Testing

extension IceCream {
    enum Flavor {
        case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio
    }
}

@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)
}
A parameterized test with one argument swift · at 0:24 ↗
import Testing

enum Ingredient: CaseIterable {
    case rice, potato, lettuce, egg
}

@Test(arguments: Ingredient.allCases)
func cook(_ ingredient: Ingredient) async throws {
    #expect(ingredient.isFresh)
    let result = try cook(ingredient)
    try #require(result.isDelicious)
}
Adding a second argument to a parameterized test swift · at 0:26 ↗
import Testing

enum Ingredient: CaseIterable {
    case rice, potato, lettuce, egg
}

enum Dish: CaseIterable {
    case onigiri, fries, salad, omelette
}

@Test(arguments: Ingredient.allCases, Dish.allCases)
func cook(_ ingredient: Ingredient, into dish: Dish) async throws {
    #expect(ingredient.isFresh)
    let result = try cook(ingredient)
    try #require(result.isDelicious)
    try #require(result == dish)
}
Using zip() on arguments swift · at 0:28 ↗
import Testing

enum Ingredient: CaseIterable {
    case rice, potato, lettuce, egg
}

enum Dish: CaseIterable {
    case onigiri, fries, salad, omelette
}

@Test(arguments: zip(Ingredient.allCases, Dish.allCases))
func cook(_ ingredient: Ingredient, into dish: Dish) async throws {
    #expect(ingredient.isFresh)
    let result = try cook(ingredient)
    try #require(result.isDelicious)
    try #require(result == dish)
}
Suites swift · at 0:29 ↗
@Suite("Various desserts") 
struct DessertTests {
    @Test func applePieCrustLayers() { /* ... */ }
    @Test func lavaCakeBakingTime() { /* ... */ }
    @Test func eggWaffleFlavors() { /* ... */ }
    @Test func cheesecakeBakingStrategy() { /* ... */ }
    @Test func mangoSagoToppings() { /* ... */ }
    @Test func bananaSplitMinimumScoop() { /* ... */ }
}
Nested suites swift · at 0:30 ↗
import Testing

@Suite("Various desserts")
struct DessertTests {
    @Suite struct WarmDesserts {
        @Test func applePieCrustLayers() { /* ... */ }
        @Test func lavaCakeBakingTime() { /* ... */ }
        @Test func eggWaffleFlavors() { /* ... */ }
    }

    @Suite struct ColdDesserts {
        @Test func cheesecakeBakingStrategy() { /* ... */ }
        @Test func mangoSagoToppings() { /* ... */ }
        @Test func bananaSplitMinimumScoop() { /* ... */ }
    }
}
Separate suites swift · at 0:31 ↗
@Suite struct DrinkTests {
    @Test func espressoExtractionTime() { /* ... */ }
    @Test func greenTeaBrewTime() { /* ... */ }
    @Test func mochaIngredientProportion() { /* ... */ }
}

@Suite struct DessertTests {
    @Test func espressoBrownieTexture() { /* ... */ }
    @Test func bungeoppangFilling() { /* ... */ }
    @Test func fruitMochiFlavors() { /* ... */ }
}
Separate suites swift · at 0:32 ↗
@Suite struct DrinkTests {
    @Test func espressoExtractionTime() { /* ... */ }
    @Test func greenTeaBrewTime() { /* ... */ }
    @Test func mochaIngredientProportion() { /* ... */ }
}

@Suite struct DessertTests {
    @Test func espressoBrownieTexture() { /* ... */ }
    @Test func bungeoppangFilling() { /* ... */ }
    @Test func fruitMochiFlavors() { /* ... */ }
}
Using a tag swift · at 0:35 ↗
import Testing 

extension Tag {
    @Tag static var caffeinated: Self
}

@Suite(.tags(.caffeinated)) struct DrinkTests {
    @Test func espressoExtractionTime() { /* ... */ }
    @Test func greenTeaBrewTime() { /* ... */ }
    @Test func mochaIngredientProportion() { /* ... */ }
}

@Suite struct DessertTests {
    @Test(.tags(.caffeinated)) func espressoBrownieTexture() { /* ... */ }
    @Test func bungeoppangFilling() { /* ... */ }
    @Test func fruitMochiFlavors() { /* ... */ }
}
Declare and use a second tag swift · at 0:36 ↗
import Testing 

extension Tag {
    @Tag static var caffeinated: Self
    @Tag static var chocolatey: Self
}

@Suite(.tags(.caffeinated)) struct DrinkTests {
    @Test func espressoExtractionTime() { /* ... */ }
    @Test func greenTeaBrewTime() { /* ... */ }
    @Test(.tags(.chocolatey)) func mochaIngredientProportion() { /* ... */ }
}

@Suite struct DessertTests {
    @Test(.tags(.caffeinated, .chocolatey)) func espressoBrownieTexture() { /* ... */ }
    @Test func bungeoppangFilling() { /* ... */ }
    @Test func fruitMochiFlavors() { /* ... */ }
}
Two tests with an unintended data dependency (not recommended) swift · at 0:37 ↗
import Testing

// ❌ This code is not concurrency-safe.

var cupcake: Cupcake? = nil

@Test func bakeCupcake() async {
    cupcake = await Cupcake.bake(toppedWith: .frosting)
    // ...
}

@Test func eatCupcake() async {
    await eat(cupcake!)
    // ...
}
Serialized trait swift · at 0:38 ↗
import Testing

@Suite("Cupcake tests", .serialized)
struct CupcakeTests {
    var cupcake: Cupcake?

    @Test func mixingIngredients() { /* ... */ }
    @Test func baking() { /* ... */ }
    @Test func decorating() { /* ... */ }
    @Test func eating() { /* ... */ }
}
Serialized trait with nested suites swift · at 0:39 ↗
import Testing

@Suite("Cupcake tests", .serialized)
struct CupcakeTests {
    var cupcake: Cupcake?

    @Suite("Mini birthday cupcake tests")
    struct MiniBirthdayCupcakeTests {
        // ...
    }

    @Test(arguments: [...]) func mixing(ingredient: Food) { /* ... */ }
    @Test func baking() { /* ... */ }
    @Test func decorating() { /* ... */ }
    @Test func eating() { /* ... */ }
}
Using async/await in a test swift · at 0:40 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await eat(cookies, with: .milk)
}
Using a function with a completion handler in a test (not recommended) swift · at 0:41 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    // ❌ This code will run after the test function returns.
    eat(cookies, with: .milk) { result, error in
        #expect(result != nil)
    }
}
Replacing a completion handler with an asynchronous function call swift · at 0:42 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await eat(cookies, with: .milk)
}
Using withCheckedThrowingContinuation swift · at 0:43 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await withCheckedThrowingContinuation { continuation in
        eat(cookies, with: .milk) { result, error in
            if let result {
                continuation.resume(returning: result)
            } else {
                continuation.resume(throwing: error)
            }
        }
    }
}
Callback that invokes more than once (not recommended) swift · at 0:44 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    // ❌ This code is not concurrency-safe.
    var cookiesEaten = 0
    try await eat(cookies, with: .milk) { cookie, crumbs in
        #expect(!crumbs.in(.milk))
        cookiesEaten += 1
    }
    #expect(cookiesEaten == 10)
}
Confirmations on callbacks that invoke more than once swift · at 0:45 ↗
import Testing

@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()
        }
    }
}
Confirmation that occurs 0 times swift · at 0:46 ↗
import Testing

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await confirmation("Ate cookies", expectedCount: 0) { ateCookie in
        try await eat(cookies, with: .milk) { cookie, crumbs in
            #expect(!crumbs.in(.milk))
            ateCookie()
        }
    }
}

Resources