Unit Testing Tutorial: Mocking Objects

In this tutorial you’ll learn how to write your own mocks, fakes and stubs to test a simple app that helps you remember your friends birthdays. By .

2.9 (8) · 1 Review

Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Writing Your First Mock

Since you’re going to be working with Core Data, add the following import to the top of PeopleListViewControllerTests.swift, right below import Birthdays:

import CoreData

Next, add the following code within the class definition of PeopleListViewControllerTests:

class MockDataProvider: NSObject, PeopleListDataProviderProtocol {
  
  var managedObjectContext: NSManagedObjectContext?
  weak var tableView: UITableView!
  func addPerson(personInfo: PersonInfo) { }
  func fetch() { }
  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }
  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    return UITableViewCell()
  }
}

This looks like a quite complicated mock class. However, this is just the bare minimum required, as you’re going to set an instance of this mock class to the dataProvider property of PeopleListViewController. Your mock class also has to conform to the PeopleListDataProviderProtocol as well as the UITableViewDataSource protocol.

Select Product\Test; your project will compile again and your zero tests will run with zero failures. Sorry — that doesn’t count at a 100% pass rate. :] But now you have everything set up for the first unit test using a mock.

It’s good practice to separate the unit tests in three parts called given, when and then. ‘Given’, sets up the environment; ‘when’ executes the code you want to test; and ‘then’ checks for the expected result.

Your test will check that the tableView property of the data provider is set after viewDidLoad() has been executed.

Add the following test to PeopleListViewControllerTests:

func testDataProviderHasTableViewPropertySetAfterLoading() {
  // given
  // 1
  let mockDataProvider = MockDataProvider()
  
  viewController.dataProvider = mockDataProvider
  
  // when
  // 2
  XCTAssertNil(mockDataProvider.tableView, "Before loading the table view should be nil")
  
  // 3
  let _ = viewController.view
  
  // then    
  // 4
  XCTAssertTrue(mockDataProvider.tableView != nil, "The table view should be set")
  XCTAssert(mockDataProvider.tableView === viewController.tableView, 
    "The table view should be set to the table view of the data source")
}

Here is what the above test is doing:

  1. Creates an instance of MockDataProvider and sets it to the dataProvider property of the view controller.
  2. Asserts that the tableView property is nil before the test.
  3. Accesses the view to trigger viewDidLoad().
  4. Asserts that the test class’ tableView property is not nil and that it is set to the tableView of the view controller.

Select Product\Test again; once the tests have finished, open the test navigator (Cmd+5 is a handy shortcut). You should see something like the following:

Screen Shot 2015-03-29 at 10.54.20

Your first test with a mock passed with flying colors! :]

Testing addPerson(_:)

The next test is to ensure selecting a contact from the list calls addPerson(_:) of the data provider.

Add the following property to the MockDataProvider class:

var addPersonGotCalled = false

Next, change addPerson(_:) to the following:

func addPerson(personInfo: PersonInfo) { addPersonGotCalled = true }

Now when you call addPerson(_:), you’ll register this in an instance of MockDataProvider by setting addPersonGotCalled to true.

You’ll have to import the AddressBookUI framework before you can add a method to test this behavior.

Add the following import right below the other imports in PeopleListViewControllerTests.swift:

import AddressBookUI

Now add the following test method with the rest of the test cases:

func testCallsAddPersonOfThePeopleDataSourceAfterAddingAPersion() {
  // given
  let mockDataSource = MockDataProvider()
  
  // 1
  viewController.dataProvider = mockDataSource
  
  // when
  // 2
  let record: ABRecord = ABPersonCreate().takeRetainedValue()
  ABRecordSetValue(record, kABPersonFirstNameProperty, "TestFirstname", nil)
  ABRecordSetValue(record, kABPersonLastNameProperty, "TestLastname", nil)
  ABRecordSetValue(record, kABPersonBirthdayProperty, NSDate(), nil)
  
  // 3
  viewController.peoplePickerNavigationController(ABPeoplePickerNavigationController(), 
    didSelectPerson: record)
  
  // then
  // 4
  XCTAssert(mockDataSource.addPersonGotCalled, "addPerson should have been called")
}

So what’s going on here?

  1. First you set the data provider of the view controller to an instance of your mock data provider.
  2. Then you create a contact by using ABPersonCreate().
  3. Here you manually call the delegate method peoplePickerNavigationController(_:didSelectPerson:). Normally, calling delegate methods manually is a code smell, but it’s fine for testing purposes.
  4. Finally you assert that addPerson(_:) was called by checking that addPersonGotCalled of the data provider mock is true.

Select Product\Test to run the tests — they should all pass. Hey, this testing thing is pretty easy!

But wait! How do you know that the tests actually test what you think they’re testing?

ragecomic_wat

Testing Your Tests

A quick way to check that a test is actually validating something is to remove the entity that the test validates.

Open PeopleListViewController.swift and comment out the following line in peoplePickerNavigationController(_:didSelectPerson:):

dataProvider?.addPerson(person)

Run the tests again; the last test you wrote should now fail. Cool — you now know that your test is actually testing something. It’s good practice to test your tests; at the very least you should test your most complicated tests to be sure they work.

ragecomic_testsOfTests

Un-comment the line to get the code back to a working state; run the tests again to make sure everything is working.

Mocking Apple Framework Classes

You may have used singletons such as NSNotificationCenter.defaultCenter() and NSUserDefaults.standardUserDefaults() — but how would you test that a notification is actually sent or that a default is set? Apple doesn’t allow you to inspect the state of these classes.

You could add the test class as an observer for the expected notifications. But this might cause your tests to become slow and unreliable since they depend on the implementation of those classes. Or the notification could be sent from another part of your code, and you wouldn’t be testing an isolated behavior.

To get around these limitations, you can use mocks in place of these singletons.

Note: When you replace Apple’s classes with mocks, it’s very important to only test the interaction with that class, not the behavior of that class, as implementation details could change at any point.

Build and run your app; add John Appleseed and David Taylor to the list of people and toggle the sorting between ‘Last Name’ and ‘First Name’. You’ll see that the order of the contacts in the list depends on the sort order of the table view.

The code that’s responsible for sorting lives in changeSort() in PeopleListViewController.swift:

@IBAction func changeSorting(sender: UISegmentedControl) {
    userDefaults.setInteger(sender.selectedSegmentIndex, forKey: "sort")
    dataProvider?.fetch()
}

This adds the selected segment index for the key sort to the user defaults and calls fetch() on the data provider. fetch() should read this new sort order from the user defaults and update the contact list, as demonstrated in PeopleListDataProvider:

public func fetch() {
  let sortKey = NSUserDefaults.standardUserDefaults().integerForKey("sort") == 0 ? "lastName" : "firstName"

  let sortDescriptor = NSSortDescriptor(key: sortKey, ascending: true)
  let sortDescriptors = [sortDescriptor]

  fetchedResultsController.fetchRequest.sortDescriptors = sortDescriptors
  var error: NSError? = nil
  if !fetchedResultsController.performFetch(&error) {
    println("error: \(error)")
  }
  tableView.reloadData()
}

PeopleListDataProvider uses an NSFetchedResultsController to fetch data from the Core Data persistent store. To change the sorting of the list, fetch() creates an array with sort descriptors and sets it to the fetch request of the fetched results controller. Then it performs a fetch to update the list and call reloadData() on the table view.

You’ll now add a test to ensure the user’s preferred sort order is correctly set in NSUserDefaults.

Open PeopleListViewControllerTests.swift and add the following class definition right below the class definition of MockDataProvider:

class MockUserDefaults: NSUserDefaults {
  var sortWasChanged = false
  override func setInteger(value: Int, forKey defaultName: String) {
    if defaultName == "sort" {
      sortWasChanged = true
    }
  }
}

MockUserDefaults is a subclass of NSUserDefaults; it has a boolean property sortWasChanged with a default value of false. It also overrides the method setInteger(_:forKey:) that changes the value of sortWasChanged to true.

Add the following test below the last test in your test class:

func testSortingCanBeChanged() {
  // given
  // 1
  let mockUserDefaults = MockUserDefaults(suiteName: "testing")!
  viewController.userDefaults = mockUserDefaults
  
  // when
  // 2
  let segmentedControl = UISegmentedControl()
  segmentedControl.selectedSegmentIndex = 0
  segmentedControl.addTarget(viewController, action: "changeSorting:", forControlEvents: .ValueChanged)
  segmentedControl.sendActionsForControlEvents(.ValueChanged)
  
  // then
  // 3
  XCTAssertTrue(mockUserDefaults.sortWasChanged, "Sort value in user defaults should be altered")
}

Here’s the play-by-play of this test:

  1. You first assign an instance of MockUserDefaults to userDefaults of the view controller; this technique is known as dependency injection).
  2. You then create an instance of UISegmentedControl, add the view controller as the target for the .ValueChanged control event and send the event.
  3. Finally, you assert that setInteger(_:forKey:) of the mock user defaults was called. Note that you don’t check if the value was actually stored in NSUserDefaults, since that’s an implementation detail.

Run your suite of tests — they should all succeed.

What about the case when you have a really complicated API or framework underneath your app, but all you really want to do is test a small feature without delving deep into the framework?

That’s when you “fake” it ’till you make it! :]