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 2 of 4 of this article. Click here to view the first page.

Writing Your First Test

In PandemicReport, ReportService is a small class for handling the CRUD logic. CRUD is an acronym for Create-Read-Update-Delete, the most common features of a persistent store. You’ll write unit tests to verify this functionality is working.

To write tests, you’ll use the XCTest framework in Xcode and subclass XCTestCase.

Start by creating a new Unit Test Case Class under PandemicReportTests and name it ReportServiceTests.swift.

Adding ReportServiceTests

Then, inside ReportServiceTests.swift, add the following code under the import XCTest:

@testable import PandemicReport
import CoreData

This code imports the app and the CoreData framework into your test case.

Next, add the following two properties to the top of the ReportServiceTests:

var reportService: ReportService!
var coreDataStack: CoreDataStack!

These properties hold a reference to the ReportService and CoreDataStack you’re testing.

Back in ReportServiceTests.swift, delete the following:

  1. setUpWithError()
  2. tearDownWithError()
  3. testExample()
  4. testPerformanceExample()

You’ll see why you don’t need them next.

The Set Up and Tear Down

Your unit tests should be isolated and repeatable. XCTestCase has two methods, setUp() and tearDown(), for setting up your test case before each run and cleaning up any test data afterwards. Since each test gets to start with a clean slate, these methods help make your tests isolated and repeatable.

Add the following code below the properties you declared:

override func setUp() {
  super.setUp()
  coreDataStack = TestCoreDataStack()
  reportService = ReportService(
    managedObjectContext: coreDataStack.mainContext,
    coreDataStack: coreDataStack)
}

Here you initialize TestCoreDataStack, which you implemented earlier, as well as ReportService. As previously stated, TestCoreDataStack uses an in-memory store and is initialized each time setUp() executes. Thus, any PandemicReport‘s created aren’t persisted from test run to test run.

On the other hand,tearDown() resets the data after each test run.

Back in ReportServiceTests, add the following:

override func tearDown() {
  super.tearDown()
  reportService = nil
  coreDataStack = nil
}

This code sets the properties to nil in preparation for the next test.

With set up and tear down in place, you can now focus on testing the CRUD of reports.

Adding a Report

Now, you’ll test the app’s existing functionality by writing a simple test to verify the add(_:numberTested:numberPositive:numberNegative:) functionality of the ReportService.

Still in ReportServiceTests, create a new method:

func testAddReport() {
  // 1
  let report = reportService.add(
    "Death Star", 
    numberTested: 1000,
    numberPositive: 999, 
    numberNegative: 1)

  // 2
  XCTAssertNotNil(report, "Report should not be nil")
  XCTAssertTrue(report.location == "Death Star")
  XCTAssertTrue(report.numberTested == 1000)
  XCTAssertTrue(report.numberPositive == 999)
  XCTAssertTrue(report.numberNegative == 1)
  XCTAssertNotNil(report.id, "id should not be nil")
  XCTAssertNotNil(report.dateReported, "dateReported should not be nil")
}

This test verifies that add(_:numberTested:numberPositive:numberNegative:) creates a PandemicReport with the specified values.

The code you added:

  1. Creates a PandemicReport.
  2. Asserts the input values match the created PandemicReport.

To run this test, click Product > Test or press Command+U as a shortcut. Alternatively, you can open the Test navigator, then select PandemicReportsTest and click play.

How to Run the Unit Test

The project builds and runs the test. You’ll see a green checkmark.

First Test Passed

Congratulations! You’ve written your first test.

Next, you’ll learn how to test asynchronous code.

Testing Asynchronous Code

Saving data is Core Data’s most important task. While your test is great, it doesn’t test if the data saves to the persistent store. It runs straight through because the app uses a separate queue for persisting data in the background.

Saving data on the main thread can block the UI, making it unresponsive. But, testing asynchronous code is a little more complicated. Specifically, XCTAssert can’t test if your data saves since you don’t know when the background task finishes.

You’ll solve this by executing the work on the thread associated with the current context by wrapping the add(_:numberTested:numberPositive:numberNegative:) call in a perform(_:). Then, you need to pair the perform(_:) with an expectation to notify the test when the save completes.

Here’s what that’ll look like.

Testing Save

Still inside ReportServiceTests, add:

func testRootContextIsSavedAfterAddingReport() {
  // 1
  let derivedContext = coreDataStack.newDerivedContext()
  reportService = ReportService(
    managedObjectContext: derivedContext,
    coreDataStack: coreDataStack)
    
  // 2
  expectation(
    forNotification: .NSManagedObjectContextDidSave,
    object: coreDataStack.mainContext) { _ in
      return true
  }

  // 3
  derivedContext.perform {
    let report = self.reportService.add(
      "Death Star 2", 
      numberTested: 600,
      numberPositive: 599, 
      numberNegative: 1)

    XCTAssertNotNil(report)
  }

  // 4
  waitForExpectations(timeout: 2.0) { error in
    XCTAssertNil(error, "Save did not occur")
  }
}

Here’s what this does:

  1. Creates a background context and a new instance of ReportService which uses that context.
  2. Creates an expectation that sends a signal to the test case when the Core Data stack sends an NSManagedObjectContextDidSave notification event.
  3. It adds a new report inside a perform(_:) block.
  4. The test waits for the signal that the report saved. The test fails if it waits longer than two seconds.

Expectations are a powerful tool when testing asynchronous code because they let you pause your code and wait for an asynchronous task to complete.

Note: To learn more about testing asynchronous operations with expectations, check out Apple’s Documentation on the topic.

Now, run the test and see a green checkmark next to it.

Testing Core Data Saves

Writing tests helps you find bugs and provide documentation on how a function behaves. But what if you wrote a broken test and it always passed? It’s time to do some TDD!

Test Driven Development (TDD)

Test Driven Development, or TDD, is a development process where you write tests before the production code. By writing your tests first, you ensure your code is testable and developed to meet all the requirements.

You start by writing a minimal amount of code to make the test pass. Then you incrementally make small changes to your functionality and repeat.

One benefit of TDD is your tests act as documentation for how your app works. As your feature set expands over time, your tests do as well and, by extension, your documentation.

As a result, unit tests are a great way to understand how a particular part of an app works. They’re useful if you need a refresher or take over a code base from another developer.

Other benefits of TDD include:

  • Code Coverage: Because you’re writing your tests before your production code, the chance of untested code is slim.
  • Confidence in refactoring: Due to that wide code coverage, and the project being broken into smaller testable units, it’s easier to make major refactors to your code base.
  • Focused: You’re writing the least amount of code to make the test pass, therefore your code base is tidy with less redundancy.

unit testing