Clean Async XCTests
Writing tests has always been a nontrivial task, especially if the code you are testing is mainly asynchronous (i.e., a tested method receives callback as one of its parameters). Although a callback-style function invocation is entirely appropriate within a program, this style doesn’t fit so well into a unit test environment.
The Definition of Clean Unit Test
I strongly believe in the rule of thumb that a unit test should be as simple as possible. It has the word “unit” in the definition for a reason: to emphasize that it shouldn’t be complicated. Based on my experience, a well-written unit test is:
1. Non-nested
If possible, you should avoid nesting and stick to line-by-line code, which is easier to read. Of course, for-loops and if-conditions do not count as nesting, but a callback certainly does. As John Sundell writes in his article The power of Result types in Swift: “…when writing tests, we don’t really want to add any code branches or conditionals. We should keep all assertions and verifications at the top level of our test.”
2. Isolated
Each unit test should complete its lifecycle within predefined boundaries, meaning that it starts, executes and finishes inside the body of a single method.
3. Behavior-Driven
A unit test should follow the GivenWhenThen contract, in which each step in a test lifecycle’s (i.e., beginning, execution and completion) has a set place.
Having introduced the principles of clean unit testing, let’s see if those rules are met by the way we test our code using XCTest
framework.
Applying clean unit testing rules to an asynchronous API test
First, let’s examine a basic asynchronous API method:
func someAPI(callback: (result: ResultType) -> ()) -> Void
If we read the signature of this method, it would go something like this: “a function that receives another function (callback) as a parameter, which also receives a parameter (of type ResultType
), while both functions return empty type (void)”.
If we unit-test this method, we would probably write the following:
func testSomeAPI() {
let expectedValue = SOME_VALUE
let expectaction = self.expectation(description: "Wait for \(expectedValue)") var result: ResultType!
someAPI {
result = $0
expectaction.fulfill()
}
waitForExpectations(timeout: 10.0)
XCTAssertEqual(result, expectedValue, "Should be \(expectedValue)")}
Note: The above code doesn’t compile. We’ll look at the working examples further.
Unfortunately, this implementation violates all the clean unit testing rules we defined earlier. In this particular example we have:
- nesting
- a non-procedural test
- boilerplate code for configuring a timeout
So, is it even possible to overcome those issues and achieve a clean unit test structure? Briefly, yes. But we first need to do a little pre-work, in order to process the whole method (a function that receives another function which, when called sometime in the future, will provide an obtained result). This method should be in an external module, so the process of waiting for a result will be transparent.
Writing such a module will help us to create a proper environment for our unit test to help it look more “unit-testy”.
Functional Async Test Wrapper
Thanks to Swift’s Higher Order Functions support, we can pass the whole async API method to another function. That function can deal with all the required configurations, such as waiting for a callback to be called, before returning the final result to our unit test. One of the possible ways to implement this is:
In the above code snippet, the async
method is placed into XCTestCase
extension. This helper method performs the same code from the above testSomeAPI
. However, this time, an API is received as an external parameter. It also returns a result as soon as the expectation fulfilled. For curious readers, this async
method is a special case of what is known in algebra as Composition of Functions: f(g(x)).
Here’s an updated testSomeAPI
which uses our newly-created async
:
func testSomeAPI() {
let expectedValue = SOME_VALUE
let result = async(someAPI)
XCTAssertEqual(result, expectedValue, "Should be \(expectedValue)")
}
As you can see, the code is entirely procedural, without any nesting, and we’ve managed to wrap around a boilerplate code for creating expectation
. Congratulations! We’ve successfully addressed all the drawbacks that we identified earlier.
API with Multiple Input Parameters
It’s most common for API to have more than one send parameter. Let’s “expand an extension
” to support an API with multiple parameters on input.
This could be done by overloading ourasync
with just one difference: that it receives additional parameters to pass them later. But, we still want to reuse the base async
implementation, as we don’t want to end up duplicating the code. To sum up, we need to find a way how to convert an API method with multiple parameters into a method with only one parameter (callback), so it has a suitable signature for calling the base async
method.
Can we even achieve this or is the only viable option to re-implementing the async
method? 🙍🏻♂️ Don’t despair, my friend. The answer lies in another functional principle called currying.
From Wikipedia: currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument. This is exactly what we need: an ability to “split” the API receiving multiple parameters and call it gradually (a.k.a “partial applying”) so that we can modify it to the signature acceptable by the base async
. Let’s define our currying helper as shown in this article, and create async
overload:
That’s how curry
function works. It receives a complete function but delays the call for it until both parameters are supplied. If we check a type of curriedCallback
we’ll see this:
And that’s exactly the type of callback parameter our base async
method expects.
The test for such an API with multiple parameters would look like this:
func testSomeAPIWithParameters() {
let expectedResult = EXP_RESULT
let result = async(input, someAPI)
XCTAssertEqual(result, expectedResult, "Should be \(expectedValue)")
}
The only difference from the previous test example is that we now pass a parameter and Swift knows to call an overloaded method.
As you might guess, we can create similar async
overloads together with curry
helpers to support multiple input parameters, aiming to cover as many different API signatures as possible. Eventually, all the overloads will call the base async
, so no code duplication needs to be done.
Note: I’m not referring to the APIs that return more than one parameter. However, I believe there are plenty of APIs like that, such as
Return<Success, Failure>
which is now supported by Swift.
I’ve built a simple Xcode project containing Test target, in which I tried to recreate a couple of functioning examples to illustrate the above concepts. Feel free to download the project.
Conclusion
This journey towards Clean Async Unit Testing has been made solely based on pure Swift principles. Perhaps this approach can’t be used broadly — in a library, for example — because it still has disadvantages such as a lack of support of more complex APIs. However, you still might want to adopt this functional way of thinking to your particular needs a moment before you try any fancy libraries for testing.
I hope you enjoyed reading this article! You’re welcome to share your thoughts and ideas, maybe how to extend this concept even more 😀. You can write to me on Twitter or leave your comments below, and I hope that you and your families are staying safe during these tough times!