Streaming is available in most browsers,
and in the Developer app.
-
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.
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
- Adding tests to your Xcode project
- Forum: Developer Tools & Services
- Improving code assessment by organizing tests into test plans
- Running tests and interpreting results
- Swift Testing
- Swift Testing GitHub repository
- Swift Testing vision document
Related Videos
WWDC24
WWDC21
-
DownloadArray
-
-
0:01 - Successful throwing function
// Expecting errors import 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 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) }
-
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 Testing enum SoftServe { case vanilla, chocolate, pineapple }
-
0:16 - Complex types
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) { /*...*/ }
-
0:17 - Conforming to CustomTestStringConvertible
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) { /*...*/ }
-
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 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) } }
-
0:22 - Swift testing parameterized tests
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) }
-
0:23 - 100% test coverage
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) }
-
0:24 - A parameterized test with one argument
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) }
-
0:26 - Adding a second argument to a parameterized test
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) }
-
0:28 - Using zip() on arguments
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) }
-
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.
An error occurred when submitting your query. Please check your Internet connection and try again.