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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

func test_shared_setsBaseURL() {
  // given
  let baseURL = URL(
    string: "https://dogpatchserver.herokuapp.com/api/v1/")!
  
  // then
  XCTAssertEqual(DogPatchClient.shared.baseURL, baseURL)
}
static let shared = DogPatchClient(
  baseURL: URL(string:"https://example.com")!,
  session: URLSession(configuration: .default),
  responseQueue: nil)
baseURL: URL(
  string:"https://dogpatchserver.herokuapp.com/api/v1/")!
func test_shared_setsSession() {
  XCTAssertTrue(
    DogPatchClient.shared.session === URLSession.shared)
}
func test_shared_setsResponseQueue() {
  XCTAssertEqual(DogPatchClient.shared.responseQueue, .main)
}
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.

func test_networkClient_setToDogPatchClient() {  
  XCTAssertTrue(sut.networkClient === DogPatchClient.shared)
}
var networkClient =
    DogPatchClient(baseURL: URL(string: "http://example.com")!,
                   session: URLSession.shared,
                   responseQueue: nil)
var networkClient = DogPatchClient.shared

Using the network client

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

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.

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

}
extension DogPatchClient: DogPatchService { }
func test_dogPatchService_declaresGetDogs() {
  // given
  let service = sut as DogPatchService

  // then
  _ = service.getDogs() { _, _ in }
}
func getDogs(completion:
  @escaping ([Dog]?, Error?) -> Void) -> URLSessionTaskProtocol

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. ;]

@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
  }
}

Using the mock network client

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

func test_refreshData_setsRequest() {
  // given
  let mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
}
Cannot assign value of type 'MockDogPatchService' to type 'DogPatchClient'
var networkClient = DogPatchClient.shared
var networkClient: DogPatchService = DogPatchClient.shared
XCTAssertTrue((sut.networkClient as? DogPatchClient)
  === DogPatchClient.shared)
// when
sut.refreshData()

// then
XCTAssertTrue(sut.dataTask ===
              mockNetworkClient.getDogsDataTask)
var dataTask: URLSessionTaskProtocol?
dataTask = networkClient.getDogs() { dogs, error in
  
}
func test_refreshData_ifAlreadyRefreshing_doesntCallAgain() {
  // given
  let mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
  
  // when
  sut.refreshData()
  sut.refreshData()
  
  // then
  XCTAssertEqual(mockNetworkClient.getDogsCallCount, 1)
}
guard dataTask == nil else { return }
var mockNetworkClient: MockDogPatchService!
func givenMockNetworkClient() {
  mockNetworkClient = MockDogPatchService()
  sut.networkClient = mockNetworkClient
}
mockNetworkClient = nil
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
givenMockNetworkClient()
func test_refreshData_completionNilsDataTask() {
  // given
  givenMockNetworkClient()  
  let dogs = givenDogs()
  
  // when
  sut.refreshData()  
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  XCTAssertNil(sut.dataTask)
}
self.dataTask = nil
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)
}
self.viewModels = dogs?.map { DogViewModel(dog: $0) } ?? []
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)
}
self.tableView.reloadData()

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

Key points

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

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now