Chapters

Hide chapters

iOS Test-Driven Development by Tutorials

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

9. Using the Network Client
Written by Joshua Greene

In the last chapter, you identified that ListingsViewController isn’t actually doing any networking. Rather, it has a // TODO comment in refreshData(). In response, you created DogPatchClient to handle networking logic. However, you haven’t used it yet.

In this chapter, you’ll update ListingsViewController to use DogPatchClient upon refreshing! Specifically, you will:

  • Add a shared instance on DogPatchClient.
  • Add a network client property on ListingsViewController.
  • Create a network client protocol.
  • Create a mock network client using the protocol.
  • Use the mock to stub and validate behavior.

Getting started

Feel free to use your project from the last chapter. If you want a fresh start, navigate to this chapter’s starter directory, open the DogPatch subdirectory and then open DogPatch.xcodeproj.

Once your project is ready, it’s time to jump in and set DogPatchClient up for networking by adding a shared instance.

Creating a shared instance

While you could instantiate DogPatchClient directly, this has disadvantages:

  1. You’d have to duplicate creation data, including the baseURL, session and responseQueue, anywhere you instantiate DogPatchClient.

  2. You’d make more network calls in parallel. As a result, you could use more network data or harm battery life.

A better alternative is to add a static shared property on DogPatchClient. This uses the “singleton plus” pattern: You’ll use the shared instance most of the time, but you’ll also create one-off DogPatchClient instances, such as in your unit tests.

Before you can write app code, you first need to write a failing test. Open DogPatchClientTests.swift and add this test right before test_init_sets_baseURL(), ignoring the compiler error like usual:

func test_shared_setsBaseURL() {
  // given
  let baseURL = URL(
    string: "https://dogpatchserver.herokuapp.com/api/v1/")!
  
  // then
  XCTAssertEqual(DogPatchClient.shared.baseURL, baseURL)
}

You first create an expected baseURL; this address corresponds to the real server URL that you’ll be calling. You then assert DogPatchClient.shared.baseURL equals this baseURL. Since you haven’t defined shared on DogPatchClient, however, this doesn’t compile.

A compiler error counts as a failing test, so you’re allowed to write app code to fix it.

Open DogPatchClient.swift, and add the following right before init(baseURL:session:responseQueue:):

static let shared = DogPatchClient(
  baseURL: URL(string:"https://example.com")!,
  session: URLSession(configuration: .default),
  responseQueue: nil)

Here you’ve defined a static shared property with dummy values for its inputs. This is enough to fix the compiler error in the unit tests.

Build and run the unit tests and, as expected, this last test fails. That’s because the baseURL set on DogPatchClient.shared is not equal to the expected baseURL. To make it pass, replace the baseURL value on shared with the following:

baseURL: URL(
  string:"https://dogpatchserver.herokuapp.com/api/v1/")!

Warning: Due to the way URL(string:relativeTo:) resolves URLs, you MUST include the trailing slash at the end of the URL string. If you don’t, the URL created within getDogs won’t include the v1 in its path and, consequently, the server will not recognize it.

Build and run the unit tests, and they should all pass now. However, you still need a couple more tests to ensure you’ve set the correct values for DogPatchClient.shared.

Add the following test below test_shared_setsBaseURL():

func test_shared_setsSession() {
  XCTAssertTrue(
    DogPatchClient.shared.session === URLSession.shared)
}

This test verifies DogPatchClient.shared.session has pointer equality to URLSession.shared. Build and run the tests, and you’ll see this fails as expected. To make it pass, open DogPatchClient.swift, and update the input argument for session to URLSession.shared. Build and run the tests again, and verify they all pass.

Finally, add the following test below test_shared_setsSession():

func test_shared_setsResponseQueue() {
  XCTAssertEqual(DogPatchClient.shared.responseQueue, .main)
}

This test checks the final property on the shared instance, responseQueue. Build and run this test, and you’ll see DogPatchClient.shared.responseQueue is currently set to nil. To fix this, update the input parameter argument for responseQueue to .main. Build and run the tests again to verify they all pass.

Ultimately, your static shared property on DogPatchClient should look like the following:

static let shared = DogPatchClient(
  baseURL: URL(
    string:"https://dogpatchserver.herokuapp.com/api/v1/")!,
  session: URLSession.shared,
  responseQueue: .main)

Adding a network client property

Next, you need to add a networkClient property to ListingsViewController. Before you can write app code, of course, you need a failing test.

Open ListingsViewControllerTests.swift and add the following right after // MARK: - Instance Properties - Tests:

func test_networkClient_setToDogPatchClient() {  
  XCTAssertTrue(sut.networkClient === DogPatchClient.shared)
}

You assert that sut.networkClient has pointer equality to DogPatchClient.shared. Since you haven’t defined networkClient on ListViewController, this test won’t compile yet.

To fix this, open ListingsViewController.swift and add the following property right after // MARK: - Instance Properties:

var networkClient =
    DogPatchClient(baseURL: URL(string: "http://example.com")!,
                   session: URLSession.shared,
                   responseQueue: nil)

You declare this as a var to allow your tests to replace it with a mock object later on. By defining this property, you’ve also fixed the compiler error.

Build and run the unit tests and you’ll see this test fails because networkClient isn’t set to DogPatchClient .shared. To make it pass, replace the declaration for var networkClient inside ListingsViewController.swift with the following:

var networkClient = DogPatchClient.shared

Build and run your tests again to verify they all pass.

Using the network client

While you could use DogPatchClient directly in your unit tests, this has several drawbacks:

  • You’d make real network calls that’d require an internet connection.
  • The tests would fail if an internet connection wasn’t available or the server was down.
  • You wouldn’t be able to predict the network response in advance, so you couldn’t verify the values are what you expected.
  • Your unit tests would be slow to run because each would need to wait for a network response.

Fortunately, there’s a better option: Use a mock network client. This lets you avoid making real network calls while completely controlling the response results.

There are two ways you can create a mock network client in Swift:

  1. You can create a mock by subclassing DogPatchClient and overriding each of its methods. This works, but you may accidentally make real network calls if you forget to override a method. You may also cause side effects, such as caching fake network responses.

  2. Similar to how you mocked URLSession, you can create a network client protocol and use this instead of DogPatchClient directly. As a result, you’d eliminate the possibility of making real network calls or causing side effects. Nice! The only downside is that you need to create an extra protocol, but this is pretty quick and easy to do.

In general, you should prefer to create a mock network client using a protocol over subclassing-and-overriding.

One reason you might choose to subclass-and-override is if your app is tightly coupled to the network client or its related types. For example, if you’re dealing with a legacy app that has a lot of untested code. Even then, you should strive to replace this using a protocol in the long run.

Creating the network client protocol

What should you put in the network client protocol? Any methods and properties that callers need to use! In turn, you’ll be able to use your mock to validate that you’re calling these correctly.

Okay, that’s enough theory! You’re ready to TDD the protocol now.

As always, you’ll write a test first. Open DogPatchClientTests.swift and add the following method, right before test_shared_setsBaseURL(), ignoring the resulting compiler error:

func test_conformsTo_DogPatchService() {
  XCTAssertTrue((sut as AnyObject) is DogPatchService)
}

You cast sut as AnyObject to prevent a compiler warning, and you then assert sut is DogPathcService. However, this causes a compiler error because you haven’t defined DogPatchService yet.

To fix this, open DogPatchClient.swift and add the following, right before the class declaration:

protocol DogPatchService {

}

Build and run the unit tests and, as expected, the last test will fail. To make it pass, add the following to the end of DogPatchClient.swift, after the closing class curly brace:

extension DogPatchClient: DogPatchService { }

Build and run the tests again, and verify they all pass.

This protocol isn’t very useful yet because it doesn’t have any methods. For this, add the following test below test_conformsTo_DogPatchService():

func test_dogPatchService_declaresGetDogs() {
  // given
  let service = sut as DogPatchService

  // then
  _ = service.getDogs() { _, _ in }
}

This test won’t compile because DogPatchService doesn’t know anything about getDogs. To fix this, adding the following inside the DogPatchService protocol:

func getDogs(completion:
  @escaping ([Dog]?, Error?) -> Void) -> URLSessionTaskProtocol

Build and run your tests now, and they should all pass.

But wait! Won’t this test result in real networking calls being made? No, if you check the setUp(), you’ll recall that you passed an instance of MockSession to create DogPatchClient and set this to sut. Since MockSession doesn’t make any real networking calls, you can freely call methods on DogPatchClient without worry.

Are there any other properties or methods you should add to DogPatchService? For example, what about init(baseURL:session:responseQueue:) or the shared property?

No, you don’t need to add these because they are implementation details. A consumer doesn’t need to know how you constructed its dependency. Rather, it only need to know what behavior the dependency provides. This, in turn, defines which methods and properties go into the protocol.

For now, this one method is all you need in DogPatchService!

Creating the mock network client

You next need to create the mock network client. Your first step is to write a test for… Oh, wait! You don’t need a test. ;]

Just like MockURLSession, your mock network client won’t be part of your app code. Instead, it enables you to write unit tests, and this in turn enables you to write app code. Okay, carry on then…!

Under DogPatchTests/Test Types/Mocks, create a new Swift File called MockDogPatchService, and replace its contents with the following:

@testable import DogPatch
import Foundation

// 1
class MockDogPatchService: DogPatchService {
      
  // 2
  var baseURL = URL(string: "https://example.com/api/")!
  var getDogsCallCount = 0
  var getDogsCompletion: (([Dog]?, Error?) -> Void)!
  lazy var getDogsDataTask = MockURLSessionTask(
    completionHandler: { _, _, _ in },
    url: URL(string: "dogs", relativeTo: baseURL)!,
    queue: nil)
      
  // 3
  func getDogs(completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionTaskProtocol {
      getDogsCallCount += 1
      getDogsCompletion = completion
      return getDogsDataTask
  }
}

Here’s what you’ve done:

  1. You create a new type for MockDogPatchService that conforms to DogPatchService.

  2. You add properties for baseURL, getDogsCallCount, getDogsCompletion and getDogsDataTask. You’ll use them to verify the mock gets called as expected and return stubbed responses.

  3. You implement getDogs(completion:), which DogPatchService requires. Whenever it’s called, you increment getDogsCallCount, set getDogsCompletion and return getDogsDataTask.

Fantastic, you implemented this mock like a pro! It mirrors how DogPatchClient works, but it allows you to fully control the response that’s returned, doesn’t require a network connection and doesn’t have any network delay. So now, it’s time to put it to work.

Using the mock network client

You’re finally ready to use the mock network client!

Open ListingsViewControllerTests.swift, and you’ll see that several tests are already included for the existing functionality. Your job is to do TDD for refreshData().

Your first test will assert that the view controller holds onto the returned data task. To do this, add the following code right after test_viewWillAppear_calls_refreshData():

func test_refreshData_setsRequest() {
  // given
  let mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
}

Here, you create mockNetworkClient and attempt to set this as sut.networkClient. Unfortunately, this causes a compiler error. What’s up with that?

Xcode actually gives a helpful error message:

Cannot assign value of type 'MockDogPatchService' to type 'DogPatchClient'

The compiler expects networkClient to be of type DogPatchClient, yet you’re attempting to set it to MockDogPatchClient, which doesn’t inherit from DogPatchClient. To fix this error, you need to explicitly set the type of networkClient to be DogPatchService.

Open ListingsViewController.swift and replace this line:

var networkClient = DogPatchClient.shared

With the following:

var networkClient: DogPatchService = DogPatchClient.shared

Both MockDogPatchService and DogPatchClient conform to DogPatchService, so this eliminates the compiler error. However, you’ll notice that test_networkClient_setToDogPatchClient no longer compiles because Swift cannot use the identical-to operator, ===, to compare DogPatchClient and DogPatchService. To fix this, you need to cast the protocol type to the object type you want to compare. Replace the contents of test_networkClient_setToDogPatchClient with:

XCTAssertTrue((sut.networkClient as? DogPatchClient)
  === DogPatchClient.shared)

Run your tests and they should all pass again.

Next, add this next code within test_refreshData_setsRequest(), right before its closing method brace:

// when
sut.refreshData()

// then
XCTAssertTrue(sut.dataTask ===
              mockNetworkClient.getDogsDataTask)

This checks that sut.dataTask is set to mockNetworkClient.getDogsDataTask after you’ve called sut.refreshData(). Since you haven’t declared dataTask on ListingsViewController, however, this doesn’t compile. To fix this, open ListingsViewController.swift and add the following right after var viewModels:

var dataTask: URLSessionTaskProtocol?

This fixes the compiler error, so build and run the tests and verify that it fails. To make it pass, you need to set dataTask whenever refreshData() is called.

Replace the contents of refreshData() with the following:

dataTask = networkClient.getDogs() { dogs, error in
  
}

Build and run the tests again, and they’ll all pass.

It’s possible refreshData could be called more than once in quick succession. For example, this could happen if the user “pulls to refresh” when a network call is already in progress.

If dataTask is already set, you don’t want to call getDogs multiple times. Add the following test after test_refreshData_setsRequest(); it ensure you’re only calling getDogs once, even if refreshData is called in quick succession:

func test_refreshData_ifAlreadyRefreshing_doesntCallAgain() {
  // given
  let mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
  
  // when
  sut.refreshData()
  sut.refreshData()
  
  // then
  XCTAssertEqual(mockNetworkClient.getDogsCallCount, 1)
}

This test calls refreshData twice in succession to simulate that scenario.

Build and run the unit tests to verify this one fails. To make it pass, open ListingsViewController.swift and add the following code just after the opening curly brace for refreshData():

guard dataTask == nil else { return }

This guard returns early if dataTask is not nil. Build and run your unit tests, and they should all now pass.

Do you see anything that needs to be refactored? The app code looks fine, but what about the unit tests? Yep, you’ve duplicated the code for setting sut.networkClient to mockNetworkClient.

To eliminate this duplication, first add this new property right after the var sut line:

var mockNetworkClient: MockDogPatchService!

Next, add the following method right after the givenDogs(count:) method:

func givenMockNetworkClient() {
  mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
}

You won’t need a MockDogPatchService for every test, so you add this helper method to create and set one. You’ll call this only from the tests that require a mock.

Then, add the following within tearDown(), right after its opening method brace:

mockNetworkClient = nil

This ensures mockNetworkClient is set to nil after each test run completes.

Finally, replace the following two lines in both test_refreshData_setsRequest and test_refreshData_ifAlreadyRefreshing_doesntCallAgain:

let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient

With this one line instead:

givenMockNetworkClient()

This gets rid of the duplicate code. Now, build and run the tests to verify they still pass.

For the next test, you need to ensure that dataTask is set back to nil after the completion is called for getDogs. Add the following test below test_refreshData_ifAlreadyRefreshing_doesntCallAgain():

func test_refreshData_completionNilsDataTask() {
  // given
  givenMockNetworkClient()  
  let dogs = givenDogs()
  
  // when
  sut.refreshData()  
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  XCTAssertNil(sut.dataTask)
}

Here’s how this test works:

  1. Within the given section, you make excellent use of your helper methods to create mockNetworkClient and dogs.
  2. Within when, you first call sut.refreshData() to set the dataTask. You then pass dogs to the getDogsCompletion closure on the mockNetworkClient. This executes the passed-in closure from ListingsViewController, and it should set the dataTask to nil.
  3. Within then, you assert that the sut.dataTask is set back to nil.

Build and run this test, and you’ll see it fails. Of course, that’s because you haven’t actually set dataTask to nil within the getDogs completion closure.

To make this pass, add this line right inside the completion closure within refreshData on ListingsViewController:

self.dataTask = nil

Build and run the tests to verify the last one now passes.

You’re now ready to test the “happy path”, which returns dogs successfully and sets it on the ListingsViewController. Add the following test below test_refreshData_completionNilsDataTask():

func test_refreshData_givenDogsResponse_setsViewModels() {
  // given
  givenMockNetworkClient()
  let dogs = givenDogs()  
  let viewModels = dogs.map { DogViewModel(dog: $0) }
  
  // when
  sut.refreshData()
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  XCTAssertEqual(sut.viewModels, viewModels)
}

Here’s how this test works:

  1. Within given, you use your helper methods to create mockNetworkClient and dogs, then you create viewModels by mapping each dog to a DogViewModel.
  2. For the when section, you call sut.refreshData() and execute the getDogsCompletion with the given dogs.
  3. Finally in then, you assert that sut.viewModels is equal to viewModels.

Build and run this test to verify it fails. You need to set viewModels on ListingsViewController to make it pass.

Add the following right after dataTask = nil within the refreshData() on ListingsViewController:

self.viewModels = dogs?.map { DogViewModel(dog: $0) } ?? []

This likewise calls map to turn dogs into a DogViewModel array. If there’s an error, dogs might be nil so you use the optional unwrap operator ? and provide the default value as an empty array.

Build and run your tests, and you’ll see this last one now passes.

Is there anything more to refactor here? Well, maybe…

At first glance, the code between test_refreshData_completionNilsDataTask and test_refreshData_completionNilsDataTask looks similar. You’ve already factored out several helper methods, however, and you’re using them here.

While you could try to refactor these tests further, you’d likely make them harder to read. Consequently, it’s okay to leave them as is! There’s always a balancing act between refactoring as much as possible and inline readability. If you’re ever in doubt, try refactoring! If it turns out the code is too difficult to read, you can always undo the change.

For the next test, you’ll verify you reload the tableView after the viewModels are set. Add the following test below test_refreshData_givenDogsResponse_setsViewModels():

func test_refreshData_givenDogsResponse_reloadsTableView() {
  // given
  givenMockNetworkClient()
  let dogs = givenDogs()
  
  // 1
  class MockTableView: UITableView {
    var calledReloadData = false
    override func reloadData() {
      calledReloadData = true
    }
  }
  // 2
  let mockTableView = MockTableView()
  sut.tableView = mockTableView
  
  // when
  sut.refreshData()
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  
  // 3
  XCTAssertTrue(mockTableView.calledReloadData)
}

There are three significant parts to this test:

  1. First, you create a MockTableView to override reloadData(). Inside that, you update a Boolean for calledReloadData.
  2. Next, you create a new instance for mockTableView and set this as sut.tableView to ensure it’s used.
  3. Finally, after you’ve called refreshData() and executed the getDogsCompletion, you assert that mockTableView.calledReloadData is true.

Build and run the unit tests, and you’ll see this test fails because you don’t currently call reloadData on the tableView. To get this to pass, add the following line, right after setting viewModels within refreshData() on ListingsViewController:

self.tableView.reloadData()

Build and run the tests again, and this last one will now pass.

Okay, you’re finally ready to check out the app. Build and run it! You’ll see that the dogs… don’t show?!

Instead, the view controller shows an error screen, and if you “pull down to refresh,” you’ll see the “loading indicator” never disappears.

This is because of the way that you implemented tableView(_:numberOfRowsInSection:): It considers whether or not the tableView is refreshing.

To fix this, you need to begin and end refreshing on the table view’s refreshControl. Add the following test below test_refreshData_givenDogsResponse_reloadsTableView():

func test_refreshData_beginsRefreshing() {
  // given
  givenMockNetworkClient()
  
  // when
  sut.refreshData()
  
  // then
  XCTAssertTrue(sut.tableView.refreshControl!.isRefreshing)
}

This test verifies that isRefreshing on the refreshControl is true after calling refreshData(). Build and run this test, and it will fail because you haven’t started refreshing yet.

To fix this, add the following line right after the guard statement within refreshData() on ListingsViewController:

tableView.refreshControl?.beginRefreshing()

Build and run the test again, and they’ll all pass.

Lastly, you need to end refreshing whenever your code calls the completion closure. Add the following test to verify this below test_refreshData_beginsRefreshing():

func test_refreshData_givenDogsResponse_endsRefreshing() {
  // given
  givenMockNetworkClient()
  let dogs = givenDogs()
  
  // when
  sut.refreshData()
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  XCTAssertFalse(sut.tableView.refreshControl!.isRefreshing)
}

This test calls refreshData(), executes the getDogsCompletion closure and asserts that isRefreshing on the refreshControl is false. Build and run this test, and it will fail because you haven’t actually finished refreshing yet.

To make it pass, add the following line right after setting viewModels within refreshData() on ListingsViewController:

self.tableView.refreshControl?.endRefreshing()

Build and run your tests again, and they should all pass.

Great job! You’ve done TDD for the entire refreshData() implementation. Build and run the app to see it in action.

Key points

In this chapter, you learned how to TDD using a network client. Here are the key points you covered:

  • You created a shared instance for the network client to avoid having multiple instances throughout the app.
  • You avoided using the real network client directly in your unit tests since that would require an internet connection, which would cause them to be slower and make testing responses harder. You used a mock network client instead.
  • You learned why it’s better to create a mock network client by implementing a protocol, instead of subclassing and overriding. By doing so, you avoided accidentally making real network calls and side effects such as caching.

You’re now able to display network results on screen! Wouldn’t it be nice if you could also see the images of the pups instead of just a placeholder image? You bet it would! In the next chapter, you’ll learn how to create an image client to help you do just that.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.