Improve your Swift unit tests with async-like helpers

Take away the ugliness of expectations for dealing with completions using async-like helpers and lambda functions.

I wish async were ready in Swift.

OK, it’s ready as of Swift 5.5! But only for iOS 15 (at the time of writing), which doesn’t really help much for most app development, since at this point you should probably still support at least one version previous.

Completion handlers are just too ugly — they lead to “pyramids of doom”, and worse a lot of error handling that would be better handled by throwing errors.

For writing unit tests in XCTest , there is at least some good news — you can clean up all those completions with a few simple helper functions.

Dealing with completions in XCTest

Let’s say you have some function that does some mysterious async task, like reading from a database:

It’s hello world , in two parts. And now you write a unit test for it. The usual way to do it is using expectations. You create anXCTestExpectation , you make the database call and fulfill the expectation when it is successful, with some timeout — like this:

This is OK…ish:

  • It’s way too long.
  • It nests the XCTAssertEqual calls.
  • It’s error prone, especially if we need to handle errors at each completion.

Flattened completions

In the spirit of nice and flat async calls, let’s unwrap it:

Note that theXCTAssertEqual is not in the completion handler— this again puts you on the path of the pyramid of doom, with lots of nested async calls. So I prefer to have one expectation for each async function, so the hierarchy of function calls stays nice and flat.

Clean it up with helper functions

This is on the right path, but it’s way too long. So let’s clean it up with a convenient helper function:

This is a pretty neat generic/template function. It takes as first argument any function which has a completion handler that returns a single object T. It creates the expectation, calls the async function, steals the result from the completion handler, and tries to give it back to you.

Use it like this:

That is really slick and short! It reads more like the await call for an async function.

You can also specify the timeout with the second parameter for calls that take longer.

Functions with arguments and completion handlers: enter lambdas

Most functions you have also take arguments. So how do those work in this case?

Enter lambda functions — for every such database call, before calling our asynclike_obj method, create a lambda function to pass as the argument. For example:

A little less clean than when we don’t have arguments at all, but still much nicer than working with expectations and a nested hierarchy in our tests.

Working with results

This is probably the biggest deal — Result are the most common way to pass errors out of completion handlers:

But… they require handling the results in a switch case. And that is just. too. much. effort.

Enter a convenient helper:

This is very slick — the last line tries to get the object that’s passed in case of success , or otherwise failure gets stuck there.

Here’s how to use it:

A special case for void, if you like

As a final note, completions that just return void could be handled with the asynclike_obj helper method. However, they can also be cleaned up a little into a non-throwing version:

Happy testing! Can’t wait for the age of async to really take over.


Oliver K. Ernst
November 12, 2021

Read this on Medium