Even though the unit test we wrote in the previous episode passes, we quickly discovered that it gives us a false sense of confidence. The unit test passes because it is executed synchronously. To unit test the networking layer, we need to replace the synchronous unit test with an asynchronous unit test. Let me show you how that works.
Writing an Asynchronous Unit Test
The anatomy of an asynchronous unit test is quite different from that of a synchronous unit test. We can break an asynchronous unit test up into three components. First, we define one or more expectations. Second, we fulfill each expectation at the appropriate time. Third, we instruct the asynchronous unit test to wait for the expectations to be fulfilled. The asynchronous unit test passes when every expectation we defined is fulfilled.
We start by defining an expectation, an instance of the XCTestExpectation class. The initializer accepts a description as its only argument. The description of the expectation is included in the test log and it is helpful to debug any issues we may encounter.
func testEpisodes() throws {
let expectation = XCTestExpectation(description: "Fetch Episodes")
let apiClient = APIClient(accessTokenProvider: MockAccessTokenProvider())
...
}
The XCTestCase class defines a few convenience methods to define expectations. The convenience method I use most often is expectation(description:). Both approaches work fine, but there is a subtle difference I discuss in a moment.
func testEpisodes() throws {
let expectation = self.expectation(description: "Fetch Episodes")
let apiClient = APIClient(accessTokenProvider: MockAccessTokenProvider())
...
}
As I mentioned earlier, an asynchronous unit test passes when every expectation we define is fulfilled. We fulfill an expectation by invoking its fulfill() method. In the testEpisodes() method, we fulfill the expectation in the completion handler we pass to the sink(receiveCompletion:receiveValue:) method. Because we expect the request to the API to succeed, we only fulfill the expectation if the Completion object is equal to finished.
func testEpisodes() throws {
let expectation = self.expectation(description: "Fetch Episodes")
let apiClient = APIClient(accessTokenProvider: MockAccessTokenProvider())
apiClient.episodes()
.sink { completion in
switch completion {
case .finished:
expectation.fulfill()
case .failure:
XCTFail("Request Should Succeed")
}
} receiveValue: { episodes in
XCTAssertEqual(episodes.count, 25)
}.store(in: &subscriptions)
}
To wrap up the asynchronous unit test, we need to instruct the unit test to wait for the expectation to be fulfilled. The XCTestCase class defines a few methods to do this. The most commonly used is the wait(for:timeout:) method. It accepts an array of expectations as its first argument and a timeout as its second argument. The XCTestCase class also defines a variant that accepts a boolean value, enforceOrder, as a third argument. The enforceOrder argument defines whether the expectations that are passed to the wait(for:timeout:enforceOrder:) method need to be fulfilled in order.
The method we invoke to wait for the expectations to be fulfilled is the waitForExpectations(timeout:handler:) method. It accepts a timeout as its first argument and a completion handler as its second argument. This method doesn't accept an array of expectations. The method waits for the expectations that are created using one of the convenience methods the XCTestCase class defines. This is convenient, but note that it isn't possible to enforce the order of the expectations. Let's invoke the waitForExpectations(timeout:handler:) method to complete the asynchronous unit test.
func testEpisodes() throws {
let expectation = self.expectation(description: "Fetch Episodes")
let apiClient = APIClient(accessTokenProvider: MockAccessTokenProvider())
apiClient.episodes()
.sink { completion in
switch completion {
case .finished:
expectation.fulfill()
case .failure:
XCTFail("Request Should Succeed")
}
} receiveValue: { episodes in
XCTAssertEqual(episodes.count, 25)
}.store(in: &subscriptions)
waitForExpectations(timeout: 10.0)
}
We don't pass a completion handler to the waitForExpectations(timeout:handler:) method. This is fine because the completion handler is optional. The refactored unit test is ready to be executed. Click the diamond in the gutter on the left to execute the unit test. The unit test should pass without issues.
We can verify that the unit test waits until the expectation we defined is fulfilled by adding a breakpoint to the completion handler. Run the unit test one more time. The breakpoint should be hit before the unit test turns green.

Xcode automatically creates a report we can inspect. Select the Report Navigator on the left to browse the latest reports. Xcode creates a report for the build and a report for the test run. Click the test log to inspect the output of the test run.

Exploring Expectations
The XCTestExpectation class declares a handful of properties to configure the asynchronous unit test you write. The isInverted property is useful if you want to verify that a given codepath isn't triggered. In other words, by setting the isInverted property of an expectation to true, you expect the expectation to not be fulfilled. This is useful in some situations, but know that it slows down the test suite because the unit test waits for the duration of the timeout you pass to the wait(for:timeout:enforceOrder:) method or the waitForExpectations(timeout:handler:) method.
Expectations created with the convenience methods of the XCTestCase class are expected to be fulfilled once and only once. If an expectation is fulfilled multiple times, an assertion is raised, failing the unit test. You sometimes know that an expectation will be fulfilled multiple times. If you know how many times an expectation will be fulfilled, you can set the expectedFulfillmentCount property of the expectation. If you don't know how often an expectation will be fulfilled and don't want the unit test to fail because of an expectation being fulfilled multiple times, you can set the assertForOverFulfill property to false to avoid an assertion from being raised.
Stubbing the API
Even though the refactored unit test passes, there is no guarantee it passes a week from now. If the Cocoacasts API is down for maintenance or the device that runs the unit test has a flaky network connection, the unit test might fail due to a timeout. As I mentioned in the previous episode, the Cocoacasts API and the network connection of the device that runs the unit test are dependencies of the unit test and that makes it brittle.
We can resolve these issues by stubbing the API. There are a number of libraries we can use to stub the Cocoacasts API. The one I have been using most over the years is OHHTTPStubs. The library is written in Objective-C, but it is compatible with Swift.
There are a few more reasons why we may want to stub the API. In the value handler of the sink(receiveCompletion:receiveValue:) method, we assert that the number of episodes is equal to 25. The unit test passes for now, but it might fail tomorrow if the Cocoacasts API returns more or less episodes.
The testEpisodes() method tests the happy path, that is, we expect the request to succeed. This isn't sufficient. We also need to write unit tests for the unhappy paths. That is one of the strengths of a test suite. We have the ability to test scenarios that are difficult or impossible to test manually. By stubbing the API, we define what response the API client receives and that enables us to test a broader range of scenarios, including failures.
What's Next?
You learned how to write an asynchronous unit test, but it doesn't stop there. We need the ability to stub the Cocoacasts API to guarantee the outcome of the test suite we are creating is predictable. By stubbing the Cocoacasts API, we will gain a number of additional benefits, including speed and the ability to test a broader range of scenarios. In the next episode, we install the OHHTTPStubs library and stub the Cocoacasts API.