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!