Chapters

Hide chapters

iOS Test-Driven Development by Tutorials

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

9. Using the Network Client
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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, your job is to update ListingsViewController to use DogPatchClient to actually network! Specifically, you’ll:

  • Add a shared instance to DogPatchClient.
  • Add a property for the network client 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(),
  responseQueue: nil)
baseURL: URL(
  string:"https://dogpatchserver.herokuapp.com/api/v1/")!
func test_shared_setsSession() {
  // given
  let session = URLSession.shared
      
  // then
  XCTAssertEqual(DogPatchClient.shared.session, session)
}
func test_shared_setsResponseQueue() {
  // given
  let responseQueue = DispatchQueue.main
      
  // then
  XCTAssertEqual(DogPatchClient.shared.responseQueue, 
    responseQueue)
}
static let shared =
  DogPatchClient(
    baseURL: URL(string: 
      "https://dogpatchserver.herokuapp.com/api/v1/")!,
    session: .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(),
                   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 consumers 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) -> URLSessionDataTask

Creating the mock network client

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

// 1
@testable import DogPatch
import Foundation

// 2
class MockDogPatchService: DogPatchService {
    
  // 3
  var getDogsCallCount = 0
  var getDogsDataTask = URLSessionDataTask()
  var getDogsCompletion: (([Dog]?, Error?) -> Void)!
  
  // 4
  func getDogs(
    completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask {
    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
XCTAssertEqual(sut.dataTask, mockNetworkClient.getDogsDataTask)
var dataTask: URLSessionDataTask?
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
  // 1
  givenMockNetworkClient()  
  let dogs = givenDogs()
  
  // when
  // 2
  sut.refreshData()
  
  // 3
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  // 4
  XCTAssertNil(sut.dataTask)
}
self.dataTask = nil
func test_refreshData_givenDogsResponse_setsViewModels() {
  // given
  // 1
  givenMockNetworkClient()
  let dogs = givenDogs()  
  let viewModels = dogs.map { DogViewModel(dog: $0) }
  
  // when  
  // 2
  sut.refreshData()
  mockNetworkClient.getDogsCompletion(dogs, nil)
  
  // then
  // 3
  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)
}
self.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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now