Hello guys, in this blog we will perform unit testing on core data.
Core Data
It is an object graph and persistence framework provided by Apple which is use to manage the model layer objects in your application.
The Core Data stack handles all of the interactions with the external data store and consists of 3 primary tools:
Managed Object Model,
It describe the schema that you will be using in your application.
Persistent Store Coordinator
It verifies that the data is in a consistent state that matches the definition on the model layer and is responsible for extracting instances of entities from that data.
Managed Object Context.
It is responsible for managing objects created and returned using Core Data. It operates in-memory, not only for speed, but also to keep your model objects’ attributes updated across your application.
Unit Testing
They are automated tests that run and validate a piece of code to make sure it behaves as intended and meets its design.
They have their own target in Xcode and are written using the XCTest Framework. A sub class of XCTestCase contains test methods to run in which only the methods starting with “test” will be parsed by Xcode and available to run.
A useful test meet 3 criteria:
- The test must be able to fail
- The test must be able to pass
- The test must be refactored and kept simple.
Let’s dive into code and see how unit testing is performed.
First Add Unit Testing target in your project which contain Core Data implementation.
Now You have to enter name of you target file and click Finish.
Now we are going to write some test case for our Core Data model.
Note:- We have implemented Core Data logic using singleton Pattern. If you want to know more about singleton pattern. Follow this link.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import XCTest @testable import ToDo_1 class CoreDataTests: XCTestCase { override func setUp() { } override func tearDown() { } } |
In the above CoreDataTests is my class name. XCTestCase provides us two default methods which run with every test cases and those methods are listed above:
- setUp()
- tearDown()
setUp() call before running the test case. This method is best place for initialising properties and allocating resources which is used in your.
tearDown() call after the test method execution is complete.
Now we are going to write some test cases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
class CoreDataTests: XCTestCase { override func setUp() { super.setUp() CoreDataManager.shared(context: self.mockPersistantContainer.viewContext) initStubs() //Listen to the change in context NotificationCenter.default.addObserver(self, selector: #selector(contextSaved(notification:)), name: NSNotification.Name.NSManagedObjectContextDidSave , object: nil) } override func tearDown() { NotificationCenter.default.removeObserver(self) flushData() super.tearDown() } func test_create_data() { CoreDataManager.shared(context: self.mockPersistantContainer.newBackgroundContext()) //Given the name & status let name = "test" //When add New Data CoreDataManager.shared.save(name: name, completion: { (result) in XCTAssertEqual( result, true ) }) //Assert: return data item } func test_fetch_all_data() { //Given a storage with two data //When fetch let results = CoreDataManager.shared.fetchAllData() //Assert return two data items XCTAssertEqual(results?.count, 5) } func test_remove_data() { //Given a item in persistent store if let items = CoreDataManager.shared.fetchAllData(){ let item = items[0] let numberOfItems = items.count //When remove a item CoreDataManager.shared.deleteParticularData(id: item.id) { (result) in XCTAssertEqual(numberOfItemsInPersistentStore(), numberOfItems-1) } //Assert number of item - 1 } } func test_save() { //Give a data item let name = "test" _ = expectationForSaveNotification() CoreDataManager.shared.save(name: name) expectation(forNotification: NSNotification.Name(rawValue: Notification.Name.NSManagedObjectContextDidSave.rawValue), object: nil, handler: nil) waitForExpectations(timeout: 1.0, handler: nil) } //MARK: mock in-memory persistant store lazy var managedObjectModel: NSManagedObjectModel = { let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: type(of: self))] )! return managedObjectModel }() lazy var mockPersistantContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MVVM", managedObjectModel: self.managedObjectModel) let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType description.shouldAddStoreAsynchronously = false // Make it simpler in test env container.persistentStoreDescriptions = [description] container.loadPersistentStores { (description, error) in // Check if the data store is in memory precondition( description.type == NSInMemoryStoreType ) // Check if creating container wrong if let error = error { fatalError("Create an in-mem coordinator failed \(error)") } } return container }() //MARK: Convinient function for notification var saveNotificationCompleteHandler: ((Notification)->())? func expectationForSaveNotification() -> XCTestExpectation { let expect = expectation(description: "Context Saved") waitForSavedNotification { (notification) in expect.fulfill() } return expect } func waitForSavedNotification(completeHandler: @escaping ((Notification)->()) ) { saveNotificationCompleteHandler = completeHandler } func contextSaved( notification: Notification ) { print("\(notification)") saveNotificationCompleteHandler?(notification) } } //MARK: Creat some fakes extension CoreDataTests { func initStubs() { CoreDataManager.shared.save(name: "text1") CoreDataManager.shared.save(name: "text2") CoreDataManager.shared.save(name: "text3") CoreDataManager.shared.save(name: "text4") CoreDataManager.shared.save(name: "text5") } func flushData() { let fetchRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest<NSFetchRequestResult>(entityName: "CoredataList") let objs = try! mockPersistantContainer.viewContext.fetch(fetchRequest) for case let obj as NSManagedObject in objs { mockPersistantContainer.viewContext.delete(obj) } try! mockPersistantContainer.viewContext.save() } func numberOfItemsInPersistentStore() -> Int { let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CoredataList") let results = try! mockPersistantContainer.viewContext.fetch(request) return results.count } } |
In the above code you can see that, in setUp() method we are initialising connection with core data and in tearDown() we are de-initialising connection with core data. These steps will follow for all test cases.
In test_create_data() we are testing create operation in Core data.
In test_remove_data() we are testing delete operation in Core data.
In test_fetch_all_data() we are testing fetching operation in Core data.
In test_save() we are saving data in Core data.
Now we will run test cases for checking status of unit testing.
Press Window + U to run all test cases or if you want to run particular test case then click on diamond icon near func keyword of test methods.
Conclusion
We should preform Unit testing because it is one of the earliest testing efforts performed on the code and the earlier defects are detected, the easier they are to fix.