Hello and Welcome!

If this is your first time reading one of my articles, let me introduce myself: I’m Skander, an iOS development enthusiast. My goal is to share my insights and discoveries about Swift and the Apple ecosystem, hoping you’ll find something interesting and useful.

I often write my articles while traveling by train through the stunning landscapes of France. Today, as I journey from Abbeville to Paris Nord for the Swift Connection, I invite you to join me for this latest exploration.

The Pitfalls of Asynchronous Testing in Swift: Avoiding Random Failures Due to Concurrency Issues

Recently, the use of asynchronous functions in Swift has become increasingly common, especially with the release of Swift6.

It’s always exciting to keep up with the latest technological advancements and stay current. However, this can sometimes be a real challenge, particularly in the context of large-scale projects.

Consider this simple example:

func toto() async {
    Task {
        let soso = try await manager.getFofo().value
        self.soso = soso
    }
}

As soon as I wrote this code, one question loomed large: how can I effectively test the contents of the Task block? While exploring available solutions online, I quickly stumbled upon examples like this:

func testToto() async throws {
    let task = sut.toto() // here the sut should be configured with the a manager mock
    try await task.value
    XCTAssertNotNil(sut.soso)
}


However, there’s an underlying issue with this approach. If you haven’t yet read my article on Data Races in Swift 6, I highly recommend doing so before proceeding.

For those of you who have read it, here’s my takeaway:

Test failures can occur randomly due to potential concurrency issues. This is common when working with asynchronous code, especially with Swift’s Tasks. The problem here is that the assertion XCTAssertNotNil may execute before the asynchronous task has completed, leading to intermittent failures.

This issue becomes even more pronounced when multiple tests run in parallel within your project, increasing the likelihood of race condition-related failures.

The Solution:

To avoid random test failures, we need to ensure that the asynchronous block has completed its task before executing any assertions. Apple recommends a classic approach using XCTestExpectation, which allows you to wait for the completion of an asynchronous block before proceeding.

func testToto() async throws {
    // GIVEN
    let expectation = XCTestExpectation(description: "Toto task completed")

    // Simulate the completion of the asynchronous task in the stub
    sut.manager.totoStub = { _ in
        expectation.fulfill()
        return true
    }

    // WHEN
    sut.toto()

    // THEN
    wait(for: [expectation], timeout: 1.0)
    XCTAssertNotNil(sut.soso)
}

Now, what do you think needs to be changed for our code to work correctly under Swift6?

Congratulations 🎉! The answer lies in wait(for:).

With Swift6, wait(for:) is now deprecated, as it’s no longer suitable for the new asynchronous APIs introduced with async/await. Fortunately, there’s a more modern alternative: await fulfillment(of:timeout:enforceOrder:).

This method allows you to wait for one or more XCTestExpectation instances to be fulfilled, seamlessly integrating Swift’s native async support. Here’s how to adapt our test:

func testToto() async throws {
    // GIVEN
    let expectation = XCTestExpectation(description: "Toto task completed")

    // Simulate the completion of the asynchronous task in the stub
    sut.magnager.totoStub = { _ in
        expectation.fulfill()
        return true
    }

    // WHEN
    sut.toto()

    // THEN
    try await fulfillment(of: [expectation], timeout: 1.0)
    XCTAssertNotNil(sut.soso)
}

That wraps up this blog! I hope you found these explanations helpful in understanding asynchronous testing in Swift6. Thank you for joining me on this journey through the challenges of async/await. I look forward to seeing you soon for another article, and who knows, maybe on another train ride through France. 🚆

Until next time for more Swift adventures!

Categorized in:

Swift,