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?
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.
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:
setUpWithError()
tearDownWithError()
testExample()
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:
- Creates a
PandemicReport
. - 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.
The project builds and runs the test. You’ll see a green checkmark.
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:
- Creates a background context and a new instance of
ReportService
which uses that context. - Creates an
expectation
that sends a signal to the test case when the Core Data stack sends anNSManagedObjectContextDidSave
notification event. - It adds a new report inside a
perform(_:)
block. - 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.
Now, run the test and see a green checkmark next to it.
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.