Have you used Swift Testing yet? I have touched them and am a big fan of the confirmation feature.

In Apple’s document “Migrating a test from XCTest”, confirmation replaces XCTestCase’s expectation and fulfillment.

With XCTestCase, we define an XCTestExpectation variable and communicate the completion of asynchronous processing by calling fulfill, waiting for the completion by fulfillment or wait:

class FoodTruckTests: XCTestCase {
  func testTruckEvents() async {
    let soldFood = expectation(description: "...")
    FoodTruck.shared.eventHandler = { event in
      if case .soldFood = event {
        soldFood.fulfill()
      }
    }
    await Customer().buy(.soup)
    await fulfillment(of: [soldFood])
    ...
  }
  ...
}

We should rewrite the tests by migrating this asynchronous processing to Swift Concurrency, but there are situations where that is difficult. In such cases, confirmation is helpful.

Confirmation functions similarly to expectation but does not block or pause the caller. Issue.record is called to fail the test if the confirmation test fails:

struct FoodTruckTests {
  @Test func truckEvents() async {
    await confirmation("...") { soldFood in
      FoodTruck.shared.eventHandler = { event in
        if case .soldFood = event {
          soldFood()
        }
      }
      await Customer().buy(.soup)
    }
    ...
  }
  ...
}

Checking the number of invocation times

Additionally, confirmation allows you to specify the number of times the target asynchronous processing is executed. You can use the expectedCount argument introduced here.

This feature might resonate well for those using Mockolo or the nostalgic Swift Mock Generator. I felt this expression was beautiful.

In the example below, we test that “when we bake cinnamon rolls ten times, the specified baked event should be also called ten times”:

let n = 10
await confirmation("Baked buns", expectedCount: n) { bunBaked in
  foodTruck.eventHandler = { event in
    if event == .baked(.cinnamonBun) {
      // The test fails if `bunBaked()` is called exactly ten times.
      bunBaked()
    }
  }
  await foodTruck.bake(.cinnamonBun, count: n)
}

Like the sample code, you can also pass 0 to expectedCount to check the corresponding asynchronous processing is not executed at all:

@Test func orderCalculatorEncountersNoErrors() async {
  let calculator = OrderCalculator()
  await confirmation(expectedCount: 0) { confirmation in
    calculator.errorHandler = { _ in confirmation() }
    calculator.subtotal(for: PizzaToppings(bases: []))
  }
}

This is similar to assigning true to isInverted of XCTestExpectation, but the confirmation expression seems more intuitive.

Conclusion

The confirmation feature in Swift Testing offers a modern and efficient way to handle asynchronous tests. Eliminating the need to block calls enhances code readability and maintainability.

This approach simplifies the testing process and aligns well with Swift’s concurrency model. As developers adopt Swift Concurrency, leveraging confirmation will undoubtedly lead to more robust and reliable test cases. Embrace this new feature to elevate your testing practices in Swift!

Thanks!