Unit Testing Core Data in iOS
Testing code is a crucial part of app development, and Core Data is not exempt from this. This tutorial will teach you how you can test Core Data. By Graham Connolly.
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
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 Core Data in iOS
25 mins
- Getting Started
- What is Unit Testing?
- CoreData Stack for Testing
- Adding the TestCoreDataStack
- Different Stores
- Writing Your First Test
- The Set Up and Tear Down
- Adding a Report
- Testing Asynchronous Code
- Testing Save
- Test Driven Development (TDD)
- The Red, Green, Refactor Cycle
- Fetching Reports
- Updating a Report
- Extending Functionality
- Deleting a Report
- Where to Go From Here?
The Red, Green, Refactor Cycle
A good unit test is failable, repeatable, quick to run and easy to maintain. By using TDD, you ensure your tests are worthwhile.
Developers often describe the TDD flow as the red-green-refactor cycle:
- Red: Write a test that fails first.
- Green: Write as little code as possible to make it pass.
- Refactor: Revise and optimize the code.
- Repeat: Repeat these steps until you feel the code works correctly.
With a little theory on TDD in place, it’s now time you put TDD into practice.
Fetching Reports
To get familiar with TDD, you’ll write a test that validates getReports()
. At first, the test will fail, but then you’ll work on making sure it doesn’t.
In ReportServiceTests.swift, add:
func testGetReports() {
//1
let newReport = reportService.add(
"Endor",
numberTested: 30,
numberPositive: 20,
numberNegative: 10)
//2
let getReports = reportService.getReports()
//3
XCTAssertNil(getReports)
//4
XCTAssertEqual(getReports?.isEmpty, true)
//5
XCTAssertTrue(newReport.id != getReports?.first?.id)
}
This code:
- Adds a new report and assigns it to
newReport
. - Gets all reports currently stored in Core Data and assigns them to
getReports
. - Verifies the result of
getReports
is nil. This is a failing test.getReports()
should return the report added. - Asserts the results array is empty.
- Asserts
newReport.id
isn’t equal to the first object ingetReports
.
Run the unit test. You’ll see the test fails.
Look at the results of the assert expressions in the failed test:
This test fails because report service returns the report you added. The asserts are the opposite conditions to what gets returned from getReports
. If this test did pass, then getReports()
isn’t working, or there’s a bug in the unit test.
To make the test go from red to green, replace the asserts with:
XCTAssertNotNil(getReports)
XCTAssertTrue(getReports?.count == 1)
XCTAssertTrue(newReport.id == getReports?.first?.id)
This code:
- Checks that
getReports
isn’tnil
. - Verifies the number of reports is 1.
- Asserts the
id
of the created report and the first result in the reports array are matching.
Next, rerun the unit tests. You’ll see a green checkmark.
Success! You’ve turned your failing test green, confirming the code and test are valid and not a false-positive.
On to the next one!
Updating a Report
Now, write a test that validates update(_:)
behaves as expected. Add the following test method:
func testUpdateReport() {
//1
let newReport = reportService.add(
"Snow Planet",
numberTested: 0,
numberPositive: 0,
numberNegative: 0)
//2
newReport.numberTested = 30
newReport.numberPositive = 10
newReport.numberNegative = 20
newReport.location = "Hoth"
//3
let updatedReport = reportService.update(newReport)
//4
XCTAssertFalse(newReport.id == updatedReport.id)
//5
XCTAssertFalse(updatedReport.numberTested == 30)
XCTAssertFalse(updatedReport.numberPositive == 10)
XCTAssertFalse(updatedReport.numberNegative == 20)
XCTAssertFalse(updatedReport.location == "Hoth")
}
This code:
- Creates a new report and assigns it to
newReport
. - Changes the current properties of
newReport
to new values. - Calls
update:
to save the changes made and assigns the updated value toupdatedReport
. - Asserts that the
newReport.id
doesn’t match theupdatedReport.id
. - Ensures the
updatedReport
properties don’t equal the value assigned tonewReport
.
Run the unit tests. You’ll see the test failed.
Look at the unit test and notice the five asserts failed.
They failed because the newReport
properties equal the properties of updatedReport
. In other words, you’d expect the test to fail here.
Update the asserts in testUpdateReport()
to:
XCTAssertTrue(newReport.id == updatedReport.id)
XCTAssertTrue(updatedReport.numberTested == 30)
XCTAssertTrue(updatedReport.numberPositive == 10)
XCTAssertTrue(updatedReport.numberNegative == 20)
XCTAssertTrue(updatedReport.location == "Hoth")
The updated code tests if the newReport.id
is equal to updatedReport.id
and if the updatedReport
properties are equal to those assigned to newReport
.
Rerun the tests and see they now pass.
What you’ve done so far is infinitely better than having no tests at all, but you’re still not truly following the TDD practice. To do so, you must write a test before writing the actual functionality in the app.
Extending Functionality
So far, you’ve added tests to support the existing functionality of the app. Now you’ll bring your TDD skills to the next level by extending the functionality of the service.
To do so, you’ll add the ability to delete a record. Since you’re going to follow true TDD practice this time, you must write the test before the production code.
Build and run and add a report. Once you add a report, you can delete it by swiping on the cell and tapping Delete.
But you only deleted the report from the reports
instance in ViewController.swift. The report still exists in the Core Data Store and will return when you build and run again.
To check this out, build and run again.
The report still exists because ReportService.swift doesn’t have a delete function. You’ll add this next.
But first, you must write a test has all the functionality you’d expect from the delete function.
In ReportServiceTests.swift, add:
func testDeleteReport() {
//1
let newReport = reportService.add(
"Starkiller Base",
numberTested: 100,
numberPositive: 80,
numberNegative: 20)
//2
var fetchReports = reportService.getReports()
XCTAssertTrue(fetchReports?.count == 1)
XCTAssertTrue(newReport.id == fetchReports?.first?.id)
//3
reportService.delete(newReport)
//4
fetchReports = reportService.getReports()
//5
XCTAssertTrue(fetchReports?.isEmpty ?? false)
}
This code:
- Adds a new report.
- Gets all the reports from the store which contain the report.
- Calls delete on the
reportService
to delete the report. This will fail as this method doesn’t yet exist. - Gets all the reports from the store again.
- Asserts the reports array is empty.
After adding this code, you’ll see a compile error. Currently, ReportService.swift doesn’t implement delete(_:)
. You’ll add that next.
Deleting a Report
Now, open ReportService.swift and add the following code at the end of the class:
public func delete(_ report: PandemicReport) {
// TODO: Delete record from CoreData
}
Here you added an empty declaration. Remember, one of the TDD rules is to write just enough code to make the test pass.
You resolved the compilation error. Rerun the tests and you’ll see one failed test.
The test now failed because the record isn’t deleted from the store. Currently, the delete method in ReportService.swift has an empty body so nothing is actually getting deleted. You’ll fix that next.
Back in ReportService.swift add the following implementation to delete(_:)
:
//1
managedObjectContext.delete(report)
//2
coreDataStack.saveContext(managedObjectContext)
The code:
- Removes the report from the persistent store.
- Saves the changes in the current context.
With that added, rerun the unit tests. You’ll see the green checkmark.
Good job! You added the delete functionality to the app using the TDD cycle.
To finish, go to ViewController.swift and replace tableView(_:commit:forRowAt:)
with the following:
func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath
) {
guard
let report = reports?[indexPath.row],
editingStyle == .delete
else {
return
}
reports?.remove(at: indexPath.row)
//1
reportService.delete(report)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
Here, you’re calling delete(_:)
after removing it from the reports array. Now if you swipe and delete, the report is deleted from the SQLite backed database.
Build and run to check it out.
And that’s a wrap! Good job introducing testing to a project with Core Data.