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.

4.4 (18) · 1 Review

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

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:

  1. Red: Write a test that fails first.
  2. Green: Write as little code as possible to make it pass.
  3. Refactor: Revise and optimize the code.
  4. Repeat: Repeat these steps until you feel the code works correctly.

Red, refactor, green, repeat

Note: If you’d like more information on TDD in iOS, check out our tutorial Test Driven Development Tutorial for iOS: Getting Started.

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:

  1. Adds a new report and assigns it to newReport.
  2. Gets all reports currently stored in Core Data and assigns them to getReports.
  3. Verifies the result of getReports is nil. This is a failing test. getReports() should return the report added.
  4. Asserts the results array is empty.
  5. Asserts newReport.id isn’t equal to the first object in getReports.

Run the unit test. You’ll see the test fails.

Get Reports test fails

Look at the results of the assert expressions in the failed test:
Failed Asserts Get Reports

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.

Connection errors

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:

  1. Checks that getReports isn’t nil.
  2. Verifies the number of reports is 1.
  3. 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.

Get Reports Passed

Success! You’ve turned your failing test green, confirming the code and test are valid and not a false-positive.

failed test passes

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:

  1. Creates a new report and assigns it to newReport.
  2. Changes the current properties of newReport to new values.
  3. Calls update: to save the changes made and assigns the updated value to updatedReport.
  4. Asserts that the newReport.id doesn’t match the updatedReport.id.
  5. Ensures the updatedReport properties don’t equal the value assigned to newReport.

Run the unit tests. You’ll see the test failed.

Updated Reports Test Fail

Look at the unit test and notice the five asserts failed.

Updated Reports Failed Assert

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 updatedReportproperties are equal to those assigned to newReport.

Rerun the tests and see they now pass.

Update Reports 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.

Swipe to 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.

Report Instance still persists.

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:

  1. Adds a new report.
  2. Gets all the reports from the store which contain the report.
  3. Calls delete on the reportService to delete the report. This will fail as this method doesn’t yet exist.
  4. Gets all the reports from the store again.
  5. 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.

Compilation error - ReportService has no member delete

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.

Delete Test Failed

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:

  1. Removes the report from the persistent store.
  2. Saves the changes in the current context.

With that added, rerun the unit tests. You’ll see the green checkmark.

Delete Report Test Passed

Good job! You added the delete functionality to the app using the TDD cycle.

devices_iphone-throwing-tests

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.

finished app

And that’s a wrap! Good job introducing testing to a project with Core Data.