Josh Prewer

3 Swift Async Testing Tips

Asynchronous programming is an integral part of modern software development. With its support for async/await, Swift has made it easier than ever to manage asynchronous code. However, testing asynchronous code can be challenging due to its non-linear execution flow. In this article, we will provide you with three essential tips for effectively testing asynchronous Swift code.

Use async/await in your tests

Starting with Swift 5.5, async/await is supported in the language. This makes it simpler to write and test asynchronous code. When testing async functions, you can now use async/await in your test functions, allowing you to write more natural and readable tests. By marking your test functions with 'async', you can use the 'await' keyword to call async functions and wait for their completion.

Avoid using Task.sleep inside of a unit test. There’s rarely a use case. If what you’re testing uses Task.sleep internally, you can inject it like in the below example.

var sleep: () async -> Void = {
    try await Task.sleep(nanoseconds: 100)
}

func doSomething() async {
    await sleep()
}

func testTestSomething() async throws {
    sleep = {}

    await doSomething()

    XCTAssert(assertSomething)
}

Utilize XCTestExpectation

The XCTest framework provides a useful class called XCTestExpectation that helps manage asynchronous testing. XCTestExpectation enables you to specify that you expect a certain condition to be fulfilled within a certain period. Once the expectation is set, the test will wait until the condition is met or the specified timeout is reached.

To use XCTestExpectation, follow these steps:

a. Create an expectation with a description to help identify it. b. Fulfill the expectation when the desired condition is met. c. Wait for the expectation to be fulfilled or for the specified timeout.

let expectation = XCTestExpectation(description: "wait for something to be called")
mockedDependency.asyncMethod = {
    expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)

// Test assertions
XCTAssert...

By using XCTestExpectation, you can ensure that your asynchronous tests are properly synchronized, and that your test suite will not hang indefinitely.

Be mindful of timeouts

When working with XCTestExpectation, it's important to set appropriate timeouts for your expectations. While it's tempting to use a large timeout value to ensure your tests don't fail due to timing issues, it can lead to unnecessarily long test runs. On the other hand, setting timeouts too low might cause tests to fail when they shouldn't.

Optimising for quicker tests often lead to more reliable tests. Lots of Task.sleep and complicated XCTestExpectation calls can lead to flakiness in a test.

  • Avoid Task.sleep or long XCTestExpectation timeouts. Refactoring the code to make the test simpler (or even synchronous) can improve the reliability and reduce the time of the test.
  • Monitor async tests in a for loop. These can be a major reason of slowness in a suite and easily get slower for small changes in the test or SUT

Testing asynchronous Swift code is crucial to ensuring the reliability and robustness of your software. By following these four tips – utilizing XCTestExpectation, using async/await in your tests and being mindful of timeouts – you can create a solid test suite for your asynchronous code, improving its quality and maintainability. With these practices in place, you'll be well-equipped to tackle the challenges of testing asynchronous code in Swift.

Tagged with: