avatarKewin

Summary

This context discusses testing asynchronous code in Swift using XCTestExpectation.

Abstract

This article is the second part of a series on Unit Testing in Swift, focusing on testing asynchronous code using XCTestExpectation. It begins with an example of a function that retrieves alumni data from a remote server asynchronously. The initial test for this function, which checks if the Result contains the correct list of alumni, will always succeed due to the asynchronous nature of the function. The article then introduces XCTestExpectation as a solution to this problem. Expectations allow the test to wait for an event to occur before completing the test. The article demonstrates how to create expectations, fulfill them, and wait for them to fulfill using the waitForExpectations function. It also covers inverted expectations for testing scenarios where an event is not expected to occur. The article concludes with a discussion on testing asynchronous "fire and forget" functions.

Bullet points

  • The initial test for an asynchronous function will always succeed due to the asynchronous nature of the function.
  • XCTestExpectation is introduced as a solution to test asynchronous code.
  • Expectations are created using the expectation function, fulfilled using the fulfill function, and waited for using the waitForExpectations function.
  • Inverted expectations can be used to test scenarios where an event is not expected to occur.
  • Testing asynchronous "fire and forget" functions can be achieved by injecting the queue or mocking dependencies used within the function.

šŸ“ˆ Unit Testing in Swift: Asynchronous Expectations

In this second part of the Unit Testing in Swift series, we will cover how to properly test asynchronous code using expectations. If you are new to unit testing and want to learn how to start unit testing with Swift in Xcode, I suggest you take a step back to Part 1: The Fundamentals of the series, before reading further. This part of the series is for you, if you want to learn how to properly test the asynchronous code and functionality within your application.

Let’s begin with an example!

Consider the School example from Part 1: The Fundamentals. Only the currently enrolled students of the school are kept in memory, but we wish to extend the School with a function for fetching all previous students (alumni) from a remote server. A function like that could look as such:

Several things can be improved in this function (including the abuse of the singleton design pattern) to allow for easier unit testing, but this will be covered in Part 3: Proper Architecture and Part 4: Mocking of this series. For now, let’s focus on writing unit tests for our function. Assuming that our Network always retrieves a list of alumni, our first test would be to check if the Result contains the correct list:

In our written test, we perform the retrieveAlumni call and wait for the response. We then assert the response depending on the result. If a failure was received, we tell XCTest to fail using the XCTFail() function and if a success result was received, we assert that the number of alumni retrieved is correct. The code looks fine, but this test will ALWAYS succeed no-matter what response the network is giving us. The reason behind this is that since the completion block will be triggered on another thread at a later stage, the test function itself completes before any of the assertions have been made — and by default in Xcode, a test function succeeds if no assertions are made.

Expectations

Luckily we have a solution for this problem: XCTestExpectation. The idea behind expectations is that we expect something to happen before completing the test. Since our test class extends XCTestCase we will be able to call the expectation(description:) function, in order to create an expectation for the test. But we also need to manually tell the test function to wait for the expectation to fulfill. This is done by using the waitForExpectations(timeout:handler:) function. Let’s see how this works:

Notice how we manually fulfill() the expectation — this gives us more control of the asynchronous nature of the test. In the waitForExpectations call, we additionally provide a timeout in seconds. This tells the test that it should wait for the expectations to fulfill within the given number of seconds and the test won’t complete until the expectation has either been fulfilled or the timeout has been reached.

Inverted Expectations

But what if we don’t expect something to happen? This can be tested as well with inverted expectations. To invert an expectation we simply just set the isInverted flag to true. A few years back, before the introduction of Result, we commonly saw completion handlers for both successful and failing events within a function like retrieveAlumni. If that was the case, we could test that the failure block would never be triggered, with an inverted expectation like so:

Note that for the inverted test, we still manually fulfill the expectation, but because it is inverted, we do not expect it to be fulfilled. This also means that the waitForExpectations call will meet its timeout and when it does, the test completes. For the same reason, testing with inverted expectations may significantly slow down your unit test suite, as each test with an inverted expectation has to wait for the timeout to complete.

Testing Asynchronous FAF Functions

The use of expectations like above, requires the tested function to trigger a completion block when the asynchronous task has completed. But not all functions have completion blocks — some just perform an asynchronous task in the background. These functions are typically referred to as ā€œfire and forgetā€ (FAF) functions. There are two common options of testing FAF functions. The option to choose depends on where the asynchronous queue is being created in your code.

Queues created in the function being tested

If the asynchronous queue is being created within the function your are testing, your only option is to use injection for the function. By injecting the queue instead of creating it within the function, we allow for full control of the thread and hence we are able to synchronise it before making our assertions in the test. Consider for instance the following function:

All it does is to create an asynchronous queue that waits for 1 second before setting the didComplete property to true. But if we attempt to test it we will face the same issue as we did in the beginning of this article. By injecting the queue we can, however, control the test scenario…

… by synchronising the queue within our test:

As easy as that we are able to test our FAF function. But what about the cases when the function itself does not create the queue?

Queues created by dependencies used by the FAF function

When the asynchronous queue is created by a dependency used within the function, the solution is to remove the asynchronous nature of the function itself. By ā€œmockingā€ the dependency we will be in complete control of the test and hence remove the asynchronous code without modifying the tested function itself. I am a strong advocate for the use of mocked dependencies in all unit tests, as being in complete control of the test environment is key. For the same reason Part 4: Mocking of this series is dedicated specifically to implementing mock classes to make your life much easier when writing unit tests.

Next steps… Better overall architecture!

As you may also have discovered by now, dependency injection is a pattern that allows for easier unit testing with high quality tests. Part 3: Proper Architecture of this series will go further in depth with general architectural patterns that are useful (and not so useful) to implement throughout your project, in order to enhance the overall quality of your test suite.

As always, if you have any questions or comments, feel free to reach out to me by commenting on these articles. I will reply to all messages.

Unit Testing
Swift
Asynchronous
Xcode
Xctest
Recommended from ReadMedium