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 .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unit Testing Tutorial: Mocking Objects
30 mins
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.
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.
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:
- You first assign an instance of
MockUserDefaults
touserDefaults
of the view controller; this technique is known as dependency injection). - You then create an instance of
UISegmentedControl
, add the view controller as the target for the.ValueChanged
control event and send the event. - 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 inNSUserDefaults
, 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! :]
Writing Fakes
Fakes behave like a full implementation of the classes they are faking. You use them as stand-ins for classes or structures that are too complicated to deal with for the purposes of your test.
In the case of the sample app, you don’t want to add records to and fetch them from the real Core Data persistent store in your tests. So instead, you’ll fake the Core Data persistent store.
Select the BirthdaysTests folder and go to File\New\File…. Choose an iOS\Source\Test Case Class template and click Next. Name your class it PeopleListDataProviderTests, click Next and then Create.
Again remove the following demo tests in the created test class:
func testExample() {
// ...
}
func testPerformanceExample() {
// ...
}
Add the following two imports to your class:
import Birthdays
import CoreData
Now add the following properties:
var storeCoordinator: NSPersistentStoreCoordinator!
var managedObjectContext: NSManagedObjectContext!
var managedObjectModel: NSManagedObjectModel!
var store: NSPersistentStore!
var dataProvider: PeopleListDataProvider!
Those properties contain the major components that are used in the Core Data stack. To get started with Core Data, check out our tutorial, Core Data Tutorial: Getting Started
Add the following code to setUp():
// 1
managedObjectModel = NSManagedObjectModel.mergedModelFromBundles(nil)
storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
store = storeCoordinator.addPersistentStoreWithType(NSInMemoryStoreType,
configuration: nil, URL: nil, options: nil, error: nil)
managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = storeCoordinator
// 2
dataProvider = PeopleListDataProvider()
dataProvider.managedObjectContext = managedObjectContext
Here’s what’s going on in the code above:
-
setUp()
creates a managed object context with an in-memory store. Normally the persistent store of Core Data is a file in the file system of the device. For these tests, you are creating a ‘persistent’ store in the memory of the device. - Then you create an instance of
PeopleListDataProvider
and the managed object context with the in-memory store is set as itsmanagedObjectContext
. This means your new data provider will work like the real one, but without adding or removing objects to the persistent store of the app.
Add the following two properties to PeopleListDataProviderTests
:
var tableView: UITableView!
var testRecord: PersonInfo!
Now add the following code to the end of setUp()
:
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("PeopleListViewController") as! PeopleListViewController
viewController.dataProvider = dataProvider
tableView = viewController.tableView
testRecord = PersonInfo(firstName: "TestFirstName", lastName: "TestLastName", birthday: NSDate())
This sets up the table view by instantiating the view controller from the storyboard and creates an instance of PersonInfo
that will be used in the tests.
When the test is done, you’ll need to discard the managed object context.
Replace tearDown()
with the following code:
override func tearDown() {
managedObjectContext = nil
var error: NSError? = nil
XCTAssert(storeCoordinator.removePersistentStore(store, error: &error),
"couldn't remove persistent store: \(error)")
super.tearDown()
}
This code sets the managedObjectContext
to nil to free up memory and removes the persistent store from the store coordinator. This is just basic housekeeping. You want to start each test with a fresh test store.
Now — you can write the actual test! Add the following test to your test class:
func testThatStoreIsSetUp() {
XCTAssertNotNil(store, "no persistent store")
}
This tests checks that the store is not nil
. It’s a good idea to have this check here to fail early in case the store could not be set up.
Run your tests — everything should pass.
The next test will check whether the data source provides the expected number of rows.
Add the following test to the test class:
func testOnePersonInThePersistantStoreResultsInOneRow() {
dataProvider.addPerson(testRecord)
XCTAssertEqual(tableView.dataSource!.tableView(tableView, numberOfRowsInSection: 0), 1,
"After adding one person number of rows is not 1")
}
First, you add a contact to the test store, then you assert that the number of rows is equal to 1.
Run the tests — they should all succeed.
By creating a fake “persistent” store that never writes to disk, you can keep your tests fast and your disk clean, while maintaining the confidence that when you actually run your app, everything will work as expected.
In a real test suite you could also test the number of sections and rows after adding two or more test contacts; this all depends on the level of confidence you’re attempting to reach in your project.
If you’ve ever worked with several teams at once on a project, you know that not all parts of the project are ready at the same time — but you still need to test your code. But how can you test a part of your code against something that may not exist, such as a web service or other back-end provider?
Stubs to the rescue! :]