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.
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:
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 an
XCTestExpectation , 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
- It’s error prone, especially if we need to handle errors at each completion.
In the spirit of nice and flat
async calls, let’s unwrap it:
Note that the
XCTAssertEqual 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
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.