Go further with Swift Testing - WWDC24 - Videos - Apple Developer (2024)

Table of Contents
Chapters Resources Related Videos

More Videos

Streaming is available in most browsers,
and in the Developer app.

  • Overview
  • Code
  • 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.

    Chapters

    • 0:00 - Introduction
    • 0:36 - Why we write tests
    • 0:51 - Challenges in testing
    • 1:21 - Writing expressive code
    • 1:35 - Expectations
    • 3:58 - Required expectations
    • 4:29 - Tests with known issues
    • 5:54 - Custom test descriptions
    • 7:23 - Parameterized testing
    • 12:47 - Organizing tests
    • 12:58 - Test suites
    • 13:33 - The tag trait
    • 20:38 - Xcode Cloud support
    • 21:09 - Testing in parallel
    • 21:36 - Parallel testing basics
    • 24:26 - Asynchronous conditions
    • 26:32 - Wrap up

    Resources

    Related Videos

    WWDC24

    • Meet Swift Testing

    WWDC21

    • Meet async/await in Swift
  • Download

    Array
    • 0:01 - Successful throwing function

      // Expecting errorsimport Testing@Test func brewTeaSuccessfully() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) let cupOfTea = try teaLeaves.brew(forMinutes: 3)}
    • 0:02 - Validating a successful throwing function

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

      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") }}
    • 0:04 - Validating a general error is thrown

      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! }}
    • 0:05 - Validating a type of error

      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! }}
    • 0:06 - Validating a specific error

      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! }}
    • 0:07 - Complicated validations

      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 }}
    • 0:08 - Throwing expectation

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

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

      import Testingstruct 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)}
    • 0:11 - Failing test with a throwing function

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

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

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

      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) }}
    • 0:15 - Simple enumerations

      import Testingenum SoftServe { case vanilla, chocolate, pineapple}
    • 0:16 - Complex types

      import Testingstruct 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) { /*...*/ }
    • 0:17 - Conforming to CustomTestStringConvertible

      import Testingstruct 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) { /*...*/ }
    • 0:18 - An enumeration with a computed property

      extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio var containsNuts: Bool { switch self { case .rockyRoad, .pistachio: return true default: return false } } }}
    • 0:19 - A test function for a specific case of an enumeration

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

      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)}
    • 0:21 - Parameterizing a test with a for loop (not recommended)

      import Testingextension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio }}@Testfunc doesNotContainNuts() throws { for flavor in [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip] { try #require(!flavor.containsNuts) }}
    • 0:22 - Swift testing parameterized tests

      import Testingextension 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)}
    • 0:23 - 100% test coverage

      import Testingextension 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)}
    • 0:24 - A parameterized test with one argument

      import Testingenum 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)}
    • 0:26 - Adding a second argument to a parameterized test

      import Testingenum 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)}
    • 0:28 - Using zip() on arguments

      import Testingenum 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)}
    • 0:29 - Suites

      @Suite("Various desserts") struct DessertTests { @Test func applePieCrustLayers() { /* ... */ } @Test func lavaCakeBakingTime() { /* ... */ } @Test func eggWaffleFlavors() { /* ... */ } @Test func cheesecakeBakingStrategy() { /* ... */ } @Test func mangoSagoToppings() { /* ... */ } @Test func bananaSplitMinimumScoop() { /* ... */ }}
    • 0:30 - Nested suites

      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() { /* ... */ } }}
    • 0:31 - Separate suites

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

      @Suite struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ }}@Suite struct DessertTests { @Test func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ }}
    • 0:35 - Using a tag

      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() { /* ... */ }}
    • 0:36 - Declare and use a second tag

      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() { /* ... */ }}
    • 0:37 - Two tests with an unintended data dependency (not recommended)

      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!) // ...}
    • 0:38 - Serialized trait

      import Testing@Suite("Cupcake tests", .serialized)struct CupcakeTests { var cupcake: Cupcake? @Test func mixingIngredients() { /* ... */ } @Test func baking() { /* ... */ } @Test func decorating() { /* ... */ } @Test func eating() { /* ... */ }}
    • 0:39 - Serialized trait with nested suites

      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() { /* ... */ }}
    • 0:40 - Using async/await in a test

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

      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) }}
    • 0:42 - Replacing a completion handler with an asynchronous function call

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

      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) } } }}
    • 0:44 - Callback that invokes more than once (not recommended)

      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)}
    • 0:45 - Confirmations on callbacks that invoke more than once

      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() } }}
    • 0:46 - Confirmation that occurs 0 times

      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() } }}
  • Looking for something specific? Enter a topic above and jump straight to the good stuff.

    Go further with Swift Testing - WWDC24 - Videos - Apple Developer (2024)
    Top Articles
    Latest Posts
    Article information

    Author: Rob Wisoky

    Last Updated:

    Views: 5241

    Rating: 4.8 / 5 (48 voted)

    Reviews: 87% of readers found this page helpful

    Author information

    Name: Rob Wisoky

    Birthday: 1994-09-30

    Address: 5789 Michel Vista, West Domenic, OR 80464-9452

    Phone: +97313824072371

    Job: Education Orchestrator

    Hobby: Lockpicking, Crocheting, Baton twirling, Video gaming, Jogging, Whittling, Model building

    Introduction: My name is Rob Wisoky, I am a smiling, helpful, encouraging, zealous, energetic, faithful, fantastic person who loves writing and wants to share my knowledge and understanding with you.