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

6. Dependency Injection & Mocks
Written by Michael Katz

So far, you’ve built and tested a fair amount of the app. There is one gigantic hole that you may have noticed… this “step-counting app” doesn’t yet count any steps!

In this chapter, you’ll learn how to use mocks to test code that depends on system or external services without needing to call services — the services may not be available, usable or reliable. These techniques allow you to test error conditions, like a failed save, and to isolate logic from SDKs, like Core Motion and HealthKit.

Don’t have an iPhone handy? Don’t worry; you’ll dip into functional testing using the Simulator to handle mock data.

What’s up with fakes, mocks, and stubs?

When writing tests, it’s important to isolate the SUT from other parts of the code so your tests have high confidence that they’re testing the system as described. Tests focused on edge cases or error conditions can be very difficult to write, as they often involve specific state external to the SUT. It’s also difficult to diagnose and debug tests that fail due to intermittent or inconsistent issues outside the SUT.

The way to isolate the SUT and circumvent these issues is to use test doubles: objects that stands in for real code. There are several variants of test doubles:

  • Stub: Stubs stand in for the original object and provide canned responses. These are often used to implement one method of a protocol and have empty or nil returning implementations for the others.

  • Fake: Fakes often have logic, but instead of providing real or production data, they provide test data. For example, a fake network manager might read/write from local JSON files instead of connecting over a network.

  • Mock: Mocks are used to verify behavior, that is they should have an expectation that a certain method of the mock gets called or that its state was set to an expected value. Mocks are generally expected to provide test values or behaviors.

  • Partial mock: While a regular mock is a complete substitution for a production object, a partial mock uses the production code and only overrides part of it to test the expectations. Partial mocks are usually a subclass or provide a proxy to the production object.

Understanding CMPedometer

There are a few ways of gathering activity data from the user, but the CMPedometer API in Core Motion is by far the easiest.

Using a CMPedometer is easy as:

  1. Check that the pedometer is available and the user has granted permission.
  2. Start listening for updates.
  3. Gather step and distance updates until the user pauses, completes the goal or loses to Nessie.

The pedometer object is supplied a CMPedometerHandler, which has a single callback that receives CMPedometerData (or an error). This data object has the step count and distance travelled.

Here’s the thing… you’re using TDD so using a CMPedometer is tricky, even if you have the host app run on a physical device. CMPedometer depends on the device state, which is too variable for consistent unit tests.

Give it a try. First, open PedometerTests.swift which has been added to the DataModel test case group. Next add the following below tearDown():

func testCMPedometer_whenQueries_loadsHistoricalData() {
  // given
  var error: Error?
  var data: CMPedometerData?
  let exp = expectation(description: "pedometer query returns")

  // when
  let now = Date()
  let then = now.addingTimeInterval(-1000)
  sut.queryPedometerData(from: then, to: now) {
                         pedometerData, pedometerError in
    error = pedometerError
    data = pedometerData
    exp.fulfill()
  }

  // then
  wait(for: [exp], timeout: 1)
  XCTAssertNil(error)
  XCTAssertNotNil(data)
  if let steps = data?.numberOfSteps {
    XCTAssertGreaterThan(steps.intValue, 0)
  } else {
    XCTFail("no step data")
  }
}

This test creates an expectation for a returned pedometer query, calls queryPedometerData(from:to:) to query the data and fulfill the expectation. It then asserts that the data contains at least one step.

Although this test compiles, it crashes on launch. Apple requires permission to use Core Motion. Strike #1 against using a real CMPedometer object in the tests. In order to ask for permission, a usage description is required. Open the app’s Info.plist.

Add a new row, use the key Privacy - Motion Usage Description and set the value to “Pedometer access is required to gather step and distance information.”

Build and test, and it may fail depending on if you run the app on device or Simulator, and if you’ve accepted the permission pop-up or not. The unpredictability caused by lack of control over CMPedometer makes this a pretty poor test. This sounds like a job for a mock!

Delete the PedometerTests.swift test file; you’re about do much better.

Mocking

Restating the problem

Open AppModelTests.swift, and add the following test beneath the “Pedometer” mark:

func testAppModel_whenStarted_startsPedometer() {
  //given
  givenGoalSet()
  let exp = expectation(for: NSPredicate(block:
  { thing, _ -> Bool in
    return (thing as! AppModel).pedometerStarted
  }), evaluatedWith: sut, handler: nil)

  // when
  try! sut.start()

  // then
  wait(for: [exp], timeout: 1)
  XCTAssertTrue(sut.pedometerStarted)
}

This test intends to verify that starting the app model will also start the pedometer. If you read the previous chapter, you’ll recognize the elusive XCTNSPredicateExpectation used to wait for the status change.

This test is subtly different from the previous one: It doesn’t test the pedometer object directly. Instead, the test verifies the behavior of the SUT by measuring the effect on the pedometer (as exposed through pedometerStarted).

To get this compiling, you’ll need to modify AppModel. Open AppModel.swift, add the following two vars:

let pedometer = CMPedometer()
private(set) var pedometerStarted = false

This adds a little state to keep track of the pedometer.

Next, add the following to the bottom of start():

startPedometer()

Finally, add the following extension to the bottom of the file:

// MARK: - Pedometer
extension AppModel {
  func startPedometer() {
    pedometer.startEventUpdates { event, error in
      if error == nil {
        self.pedometerStarted = true
      }
    }
  }
}

This uses the pedometer event handler callback to determine if the pedometer has started. With a CMPedometer, you can’t write a simple test to check if it’s started as that state isn’t exposed in the API. However, this callback will be called soon after starting event updates. If step counting is available, then there won’t be an error, and you’ll know it’s started.

Build and test, and this will pass if you run it on a device and have granted permission to motion data. If you run on Simulator or device without this permission granted, it’ll fail.

Mocking the pedometer

To move pass this impasse, it’s time to create the mock pedometer. In order to swap CMPedometer for it’s mock object, you’ll first need to separate the pedometer’s interface from its implementation.

To do that, you’ll make use of two classic patterns: Facade and Bridge.

First, create a new group in the app, named Pedometer. In that group, create a new Swift file, Pedometer.swift.

For now, just add the following code:

protocol Pedometer {
  func start()
}

This is the start of the Bridge protocol that will allow you to substitute any pedometer implementation for the real one.

In order to do that, you’ll have to declare conformance for CMPedometer. Create another Swift file in the group: CMPedometer+Pedometer.swift and replace its contents with the following:

import CoreMotion

extension CMPedometer: Pedometer {
  func start() {
    startEventUpdates { event, error in
      // do nothing here for now
    }
  }
}

This declares conformance to the new protocol and migrates the start behavior you implemented in startPedometer. It doesn’t do anything much yet, but will soon.

Next, open AppModel.swift and decouple AppModel from the specific implementation of CMPedometer:

  1. Change the pedometer declaration to: let pedometer: Pedometer.
  2. Remove the pedometerStarted property.
  3. Add the following initializer:
init(pedometer: Pedometer = CMPedometer()) {
  self.pedometer = pedometer
}
  1. Change startPedometer to:
func startPedometer() {
  pedometer.start()
}

The optional init parameter is where you’ll be able to replace the default CMPedometer with the mock object. The reduction of code in startPedometer is the advantage of using a Facade: You can hide the specific complexity of the CMPedometer behind a simplified interface.

Now, it’s time to create the mock!

Create a new Swift file in the Mocks group in FitNessTests named MockPedometer.swift and replace its contents with the following:

import CoreMotion
@testable import FitNess

class MockPedometer: Pedometer {
  private(set) var started: Bool = false

  func start() {
    started = true
  }

}

This creates a very different implementation of Pedometer. Its start method instead of making CoreMotion calls just sets a Bool that can be checked in a test. Here’s another value of mocking — you can spy or inspect the mock to check that the right methods were called or that its state was set appropriately.

Now, go back to AppModelTests.swift and add the following property up top and update setUp:

var mockPedometer: MockPedometer!

override func setUp() {
  super.setUp()
  mockPedometer = MockPedometer()
  sut = AppModel(pedometer: mockPedometer)
}

This creates a mock pedometer and uses it when creating the sut.

Now, go back to testAppModel_whenStarted_startsPedometer and replace it with the following:

func testAppModel_whenStarted_startsPedometer() {
  //given
  givenGoalSet()

  // when
  try! sut.start()

  // then
  XCTAssertTrue(mockPedometer.started)
}

This simplified test now tests the side effect of start on the mock object. In addition to being a simpler test, it’s guaranteed to pass regardless of the device state. Build and test, and you’ll see that it passes.

Handling error conditions

Mocks make it easy to test error conditions. If you’ve been following along so far using both Simulator and a device, you may have encountered one or both of these error states:

  • Step counting is not available on a device, such as the Simulator.
  • The user may deny permission for motion recording on device.

Dealing with no pedometer

To handle the first case, you’ll have to add functionality to detect that the pedometer is not available and to inform the user.

First, add this test in AppModelTests under the “Pedometer” mark:

func testPedometerNotAvailable_whenStarted_doesNotStart() {
 // given
 givenGoalSet()
 mockPedometer.pedometerAvailable = false

 // when
 try! sut.start()

 // then
 XCTAssertEqual(sut.appState, .notStarted)
}

This simple check just makes sure the app state doesn’t proceed to inProgress when the pedometer isn’t available.

Next, open Pedometer.swift and add the following to the protocol definition:

var pedometerAvailable: Bool { get }

This creates a var to read the availability state.

Next, open MockPedometer.swift and update MockPedometer by adding the following:

var pedometerAvailable: Bool = true

And for the real implementation — to be used by your app code — open CMPedometer+Pedometer.swift and add the following:

var pedometerAvailable: Bool {
  return CMPedometer.isStepCountingAvailable() &&
    CMPedometer.isDistanceAvailable() &&
    CMPedometer.authorizationStatus() != .restricted
}

You can see that the “real” implementation is a lot more interesting, but not controllable.

Now the test compiles, and it’s time to get it to pass.

Open AppModel.swift, find start() and add the following before appState = .inProgress:

guard pedometer.pedometerAvailable else {
  AlertCenter.instance.postAlert(alert: .noPedometer)
  return
}

Unlike the other guard statement, this condition doesn’t raise an exception; instead, it uses the new AlertCenter way of communicating with the user. The resulting error handling, where start() is called, will be a little different, and refactoring it is out of scope of this chapter.

Build and test, and it will pass now, as the new guard prevents the appState from progressing to inProgress when the pedometer isn’t available. Note that, if you run the entire suite, some other tests will now fail — you’ll circle back to those in a moment.

It’s a good idea to test the alert, as well.

Open, AppModelTests.swift and add the following below testPedometerNotAvailable_whenStarted_doesNotStart():

func testPedometerNotAvailable_whenStarted_generatesAlert() {
  // given
  givenGoalSet()
  mockPedometer.pedometerAvailable = false
  let exp = expectation(forNotification: AlertNotification.name,
                        object: nil,
                        handler: alertHandler(.noPedometer))

  // when
  try! sut.start()

  // then
  wait(for: [exp], timeout: 1)
}

This sets pedometerAvailable to false and waits for the corresponding alert. The test will pass out of the gate due to the code previously added to AppModel for displaying this alert.

Injecting dependencies

Re-run all the tests, and you will see failures in StepCountControllerTests. That’s because this new pedometerAvailable guard in AppModel is still dependent on the production CMPedometer in other tests.

One way to fix that this to make the pedometer into a variable so it can be modified for testing.

Open AppModel.swift and change the let to a var:

var pedometer: Pedometer

Next, open ViewControllers.swift and add the following to the top of loadRootViewController():

AppModel.instance.pedometer = MockPedometer()

This sets the mock pedometer when the root view controller is fetched for tests, which means any view controller test will get a mock pedometer.

Build and run all the tests, and they will now pass.

Dealing with no permission

The other error state that needs to be handled is when the user declines the permission pop-up.

Open AppModelTests.swift and add the following to the end of the class:

func testPedometerNotAuthorized_whenStarted_doesNotStart() {
  // given
  givenGoalSet()
  mockPedometer.permissionDeclined = true

  // when
  try! sut.start()

  // then
  XCTAssertEqual(sut.appState, .notStarted)
}

func testPedometerNotAuthorized_whenStarted_generatesAlert() {
  // given
  givenGoalSet()
  mockPedometer.permissionDeclined = true
  let exp = expectation(forNotification: AlertNotification.name,
                        object: nil,
                        handler: alertHandler(.notAuthorized))

  // when
  try! sut.start()

  // then
  wait(for: [exp], timeout: 1)
}

These test handling of a permissionDeclined error. The first test checks that the app state stays in .notStarted and the second checks for a user alert.

To get them to work, you need to add permissionDeclined in a few places:

First, open Pedometer.swift, and add the following to the protocol definition:

var permissionDeclined: Bool { get }

Next, open MockPedometer.swift and add the following to the mock implementation:

var permissionDeclined: Bool = false

Next, open CMPedometer+Pedometer.swift and add the following to the real implementation:

var permissionDeclined: Bool {
  return CMPedometer.authorizationStatus() == .denied
}

Finally, open AppModel.swift, and add another guard statement to start:

guard !pedometer.permissionDeclined else {
  AlertCenter.instance.postAlert(alert: .notAuthorized)
  return
}

With permissionDeclined handled, the tests will now pass.

Mocking a callback

There is another important error situation to handle. This occurs the very first time the user taps Start on a pedometer-capable device. In that case, the start flow goes ahead, but the user can decline in the permission pop-up. If the user declines, there is an error in the eventUpdates callback.

Let’s test that condition. Open AppModelTests.swift and add the following to the end of the class definition:

func testAppModel_whenDeniedAuthAfterStart_generatesAlert() {
  // given
  givenGoalSet()
  mockPedometer.error = MockPedometer.notAuthorizedError
  let exp = expectation(forNotification: AlertNotification.name,
                        object: nil,
                        handler: alertHandler(.notAuthorized))

  // when
  try! sut.start()

  // then
  wait(for: [exp], timeout: 1)
}

Unlike the previous tests, this doesn’t explicitly set permissionDeclined, so the model can attempt to start the pedometer. Instead, the test relies on passing an error to the mock to generate the alert while the pedometer is starting.

The next step is to build a way to get that error back to the SUT.

Open Pedometer.swift, change the definition of start() to the following:

func start(completion: @escaping (Error?) -> Void)

This allows for a completion callback for error handling.

Next, update CMPedometer+Pedometer.swift by replacing start with:

func start(completion: @escaping (Error?) -> Void) {
  startEventUpdates { event, error in
    completion(error)
  }
}

This forwards the error on to the completion.

Next add the error handling in AppModel.swift, by replacing startPedometer with the following:

func startPedometer() {
  pedometer.start { error in
    if let error = error {
      let alert = error.is(CMErrorMotionActivityNotAuthorized)
        ? .notAuthorized : Alert(error.localizedDescription)
      AlertCenter.instance.postAlert(alert: alert)
    }
  }
}

The closure checks if an error was returned when starting the pedometer. If it’s a CMErrorMotionActivityNotAuthorized, then it posts a notAuthorized alert; otherwise, a generic alert with the error’s message is posted.

This takes care of the production code, but you also need to update the MockPedometer.

Open MockPedometer.swift and replace start() with the following:

var error: Error?

func start(completion: @escaping (Error?) -> Void) {
  started = true
  DispatchQueue.global(qos: .default).async {
    completion(self.error)
  }
}

static let notAuthorizedError =
  NSError(domain: CMErrorDomain,
          code: Int(CMErrorMotionActivityNotAuthorized.rawValue),
          userInfo: nil)

This update will call the completion, passing its error property. For convenience, the static notAuthorizedError creates an error object that matches what is returned by Core Motion when unauthorized. This is what you used in testAppModel_whenDeniedAuthAfterStart_generatesAlert.

Build and test again, and your tests should pass.

Getting actual data

It’s time move on to handling data updates. The incoming data is the most important part of the app, and it’s crucial to have it properly mocked. The actual step and distance count are provided by CMPedometer through the aptly named CMPedometerData object. This too should be abstracted between the app and Core Motion.

Open Pedometer.swift and add the following protocol:

protocol PedometerData {
  var steps: Int { get }
  var distanceTravelled: Double { get }
}

This adds an abstraction around CMPedometerData so that the step and distance data can be mocked. Do that by creating a new .swift file in the Mocks group of the test target: MockData.swift and replacing its contents with the following:

@testable import FitNess

struct MockData: PedometerData {
  let steps: Int
  let distanceTravelled: Double
}

With this in place, open AppModelTests.swift and add the following test at the end of the class definition:

func testModel_whenPedometerUpdates_updatesDataModel() {
  // given
  givenInProgress()
  let data = MockData(steps: 100, distanceTravelled: 10)

  // when
  mockPedometer.sendData(data)

  // then
  XCTAssertEqual(sut.dataModel.steps, 100)
  XCTAssertEqual(sut.dataModel.distance, 10)
}

The test verifies that the supplied data is applied to the data model. This requires an update to MockPedometer to pass the data. First, think about how that data will eventually be passed to AppModel.

Open Pedometer.swift. In the Pedometer protocol, change the signature of start(completion:) to the following:

func start(
  dataUpdates: @escaping (PedometerData?, Error?) -> Void,
  eventUpdates: @escaping (Error?) -> Void)

The dataUpdates block will provide a means of returning PedometerData from the pedometer. eventUpdates will return events, as the old completion block did.

In MockPedometer, create two new variables to hold these callback blocks:

var updateBlock: ((Error?) -> Void)?
var dataBlock: ((PedometerData?, Error?) -> Void)?

Next, replace start(completion:) with the following:

func start(
  dataUpdates: @escaping (PedometerData?, Error?) -> Void,
  eventUpdates: @escaping (Error?) -> Void) {

  started = true
  updateBlock = eventUpdates
  dataBlock = dataUpdates
  DispatchQueue.global(qos: .default).async {
    self.updateBlock?(self.error)
  }
}

func sendData(_ data: PedometerData?) {
  dataBlock?(data, error)
}

The two blocks are saved for later use, but the updateBlock is still called as part of this method, as completion was previously. You won’t have to update any previous tests for this one, as the behavior is the same. Also added is sendData(_:), which is used by the test to call the dataBlock with the mock data.

You also need to update the CMPedometer extension for this new logic. Open CMPedometer+Pedometer.swift and change start(completion:) to the following:

func start(
  dataUpdates: @escaping (PedometerData?, Error?) -> Void,
  eventUpdates: @escaping (Error?) -> Void) {

  startEventUpdates { event, error in
    eventUpdates(error)
  }

  startUpdates(from: Date()) { data, error in
    dataUpdates(data, error)
  }
}

This preserves the previous startEventUpdates behavior, plus adds a new call to startUpdates to forward the data updates.

You also need to wrap CMPedometerData with the new PedometerData protocol. Add the following extension to bottom of the file:

extension CMPedometerData: PedometerData {

  var steps: Int {
    return numberOfSteps.intValue
  }

  var distanceTravelled: Double {
    return distance?.doubleValue ?? 0
  }
}

This forwards the CMPedometerData values as PedometerData variables.

Finally, open AppModel.swift, and replace startPedometer() with the following:

func startPedometer() {
  pedometer.start(dataUpdates: handleData,
                  eventUpdates: handleEvents)
}

func handleData(data: PedometerData?, error: Error?) {
  if let data = data {
    dataModel.steps += data.steps
    dataModel.distance += data.distanceTravelled
  }
}

func handleEvents(error: Error?) {
  if let error = error {
    let alert = error.is(CMErrorMotionActivityNotAuthorized)
      ? .notAuthorized : Alert(error.localizedDescription)
    AlertCenter.instance.postAlert(alert: alert)
  }
}

This moves the previous event handling to its own method and creates a new one to update dataModel when there is new data. You’ll notice that data update errors are not handled here. That’s left as a Challenge for you after this chapter is complete!

Build and test, and watch that green grow!

Making a functional fake

At this point it sure would be nice to see the app in action. The unit tests are useful for verifying logic but are bad at verifying you’re building a good user experience. One way to do that is to build and run on a device, but that will require you to walk around to complete the goal. That’s very time and calorie consuming. There has got to be a better way!

Enter the fake pedometer: You’ve already done the work to abstract the app from a real CMPedometer, so it’s straightforward to build a fake pedometer that speeds up time or makes up movement.

Create a new .swift file in the pedometer group: SimulatorPedometer.swift. Replace its contents with the following:

import Foundation

class SimulatorPedometer: Pedometer {

  struct Data: PedometerData {
    let steps: Int
    let distanceTravelled: Double
  }

  var pedometerAvailable: Bool = true
  var permissionDeclined: Bool = false

  var timer: Timer?
  var distance = 0.0

  var updateBlock: ((Error?) -> Void)?
  var dataBlock: ((PedometerData?, Error?) -> Void)?

  func start(
    dataUpdates: @escaping (PedometerData?, Error?) -> Void,
    eventUpdates: @escaping (Error?) -> Void) {

    updateBlock = eventUpdates
    dataBlock = dataUpdates

    timer = Timer(timeInterval: 1, repeats: true,
                  block: { timer in
      self.distance += 1
      print("updated distance: \(self.distance)")
      let data = Data(steps: 10,
                      distanceTravelled: self.distance)
      self.dataBlock?(data, nil)
    })
    RunLoop.main.add(timer!, forMode: RunLoop.Mode.default)
    updateBlock?(nil)
  }

  func stop() {
    timer?.invalidate()
    updateBlock?(nil)
    updateBlock = nil
    dataBlock = nil
  }
}

This giant block of code implements the Pedometer and PedometerData protocols. It sets up a Timer object that, once start is called, adds ten steps every second. Each time it updates, it calls dataBlock with the new data.

You’ve also added a stop method that stops the timer and cleans up. This will be used when you add the ability to pause the pedometer by tapping the Pause button.

To use the simulated pedometer in the app, open AppModel.swift, and add the following static var:

static var pedometerFactory: (() -> Pedometer) = {
  #if targetEnvironment(simulator)
  return SimulatorPedometer()
  #else
  return CMPedometer()
  #endif
}

This method creates either a SimulatorPedometer() or a CMPedometer() depending on the app’s target environment.

Next, replace init with the following:

init(pedometer: Pedometer = pedometerFactory()) {
  self.pedometer = pedometer
}

Now build and run in Simulator. Tap the settings cog in the lower-right and enter a goal of 100 steps.

Tap Start, and you’ll see alert notifications coming in!

Wiring up the chase view

Looking at the app now, that white box in the middle is a little disappointing. This is the chase view (it illustrates Nessie’s chase of the user), and hasn’t yet been wired up.

In order to test that it will accurately reflect the user’s state, you can use a partial mock. By partially mocking the chase view, you can add a little extra test functionality without interrupting its main logic. This is instead of a full mock, which replaces all functionality.

Create a new file in the Mocks group called ChaseViewPartialMock.swift and replace its contents with the following:

@testable import FitNess

class ChaseViewPartialMock: ChaseView {
  var updateStateCalled = false
  var lastRunner: Double?
  var lastNessie: Double?

  override func updateState(runner: Double, nessie: Double) {
    updateStateCalled = true
    lastRunner = runner
    lastNessie = nessie
    super.updateState(runner: runner, nessie: nessie)
  }
}

This partial mock overrides updateState(runner:nessie:) so that the values sent to it can be recorded and verified in tests. updateStateCalled can be used by tests to track that the method has been called — a common mock validation.

This class is used by StepCountController.

First open StepCountControllerTests.swift and add the following variable:

var mockChaseView: ChaseViewPartialMock!

Next, add the following lines to the bottom of setUp():

mockChaseView = ChaseViewPartialMock()
sut.chaseView = mockChaseView

Finally, add a test that verifies that the view gets updated:

func testChaseView_whenDataSent_isUpdated() {
  // given
  givenInProgress()

  // when
  let data = MockData(steps:500, distanceTravelled:10)
  (AppModel.instance.pedometer as! MockPedometer).sendData(data)

  // then
  XCTAssertTrue(mockChaseView.updateStateCalled)
  XCTAssertEqual(mockChaseView.lastRunner, 0.5)
}

This uses the mocked pedometer to send data and verifies the state on the partial mock chase view. The value for Nessie’s position isn’t checked since the code for Nessie isn’t part of the project yet.

Build and test, and you’ll see neither assert passes, because the chase view isn’t yet being updated.

Open StepCountController.swift, and add the following to viewDidLoad() to kick off this update:

NotificationCenter.default
  .addObserver(forName: DataModel.UpdateNotification,
               object: nil,
               queue: nil) { _ in
                self.updateUI()
}

This listens for data model updates and calls updateUI when there is a data update.

updateUI calls updateChaseView, which needs to calculate the location of Nessie and the runner, then update them in the view. Replace updateChaseView with with the following:

private func updateChaseView() {
  chaseView.state = AppModel.instance.appState
  let dataModel = AppModel.instance.dataModel
  let runner =
    Double(dataModel.steps) / Double(dataModel.goal ?? 10_000)
  let nessie = dataModel.nessie.distance > 0 ?
    dataModel.distance / dataModel.nessie.distance : 0
  chaseView.updateState(runner: runner, nessie: nessie)
}

This gathers the distance of the user and Nessie from the data model, computes a percent completion, and presents it to the chase view so that the avatars can be placed accordingly.

Build and test to see the test pass! Build and run to see the view in action:

Time dependencies

The final major piece missing is Nessie. She should be chasing after the user while the app is in progress. Her progress will be measured at a constant velocity. Measuring something over time? Sounds like a Timer is the answer.

Timers are notoriously hard to test: They require using expectations along with having a potentially long wait. There are few common solutions:

  1. During tests, use a very short timer (e.g., one millisecond instead of one second).
  2. Swap the timer for a mock that executes the callback immediately.
  3. Use the callback directly, and save the timing for app or user-acceptance testing.

Any of these are reasonable solutions, but you’re going to go with option #3. In NessieTests.swift, add this test:

func testNessie_whenUpdated_incrementsDistance() {
  // when
  sut.incrementDistance()

  // then
  XCTAssertEqual(sut.distance, sut.velocity)
}

This calls incrementDistance directly, just as the Timer callback does in the Nessie class. It asserts that after distance increments it is equal to the velocity.

The test doesn’t yet pass, because incrementDistance is stubbed out. Open Nessie.swift, and add the following line to incrementDistance():

distance += velocity

The distance now increments, and the test will pass.

Challenge

You’ve reached the end of the chapter, but not the end of the app. You should be able to take the testing tools you’ve learned and finish the app. Your challenge is to add the following tests and features to complete the app:

  • Complete the Pause functionality to be able to pause and resume the pedometer.

  • Wire up Nessie to app state so it can start, pause and reset appropriately. You’ll also have to give the user a little bit of a head start since both the user and Nessie will start at 0.

  • Complete the handling of data errors from the pedometer (use the Alert Center).

Key points

  • Test doubles let you test code in isolation from other systems, especially those that are part of system SDKs, rely on networking or timers.
  • Mocks let you swap in a test implementation of a class, and partial mocks let you just substitute part of a class.
  • Fakes let you supply data for testing or use in Simulator.

Where to go from here?

That’s it. Over the past few chapters, you’ve built an an app from the ground up following TDD principles.

This chapter covered using mocks to separate the test subjects from external code and events. This just scratches the surface of what’s possible. The next section will be all about using external services like network requests.

If you want to learn more about the use and history of doubles, read this excellent Martin Fowler article, “Mocks Aren’t Stubs”: https://martinfowler.com/articles/mocksArentStubs.html.

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.