I am trying to do some simple Core Data unit tests.
But from what I can see there are two persistent stores created (both sql type), so my setup(), tearDown() unit test methods are called twice, reseting the NSManagedObjectContext thus causing some issues.
Here is my code to load the stores:
func createContainer(storeType: StoreType, completion: #escaping (NSPersistentContainer) -> ()) {
let container = NSPersistentContainer(name: "Gym")
container.loadPersistentStores { description, error in
guard error == nil else {
fatalError("Failed to load store \(error!)")
}
description.type = storeType.type
completion(container)
}
}
And this is my unit test code:
import XCTest
import CoreData
#testable import Gym
class GymTests: XCTestCase {
var context: NSManagedObjectContext!
override func setUp() {
super.setUp()
createContainer(storeType: .inMemory) { container in
self.context = container.viewContext
}
}
func testAddingExercise() {
// Given
context.performChanges {
_ = Exercise.insert(into: self.context, name: "Bench Press", groups: Set(["Chest", "Triceps", "Shoulders"]))
_ = Exercise.insert(into: self.context, name: "Squats", groups: Set(["Legs"]))
_ = Exercise.insert(into: self.context, name: "Deadlifts", groups: Set(["Back", "Legs", "Arms"]))
}
// When
let exercises = Exercise.fetch(in: context)
// Then
XCTAssertEqual(exercises.count, 3)
}
override func tearDown() {
context = nil
super.tearDown()
}
}
The second time it runs my test, the context was reset in tearDown() so context will be nil when adding exercises.
Not sure exactly how is this happening. Why is it creating two stores ?
Related
I'm trying to figure out why this crashes when both unit tests are run together.
When run separately, one test at a time, everything works. But when I try to run the tests for the whole class, the second one fails with "The model configuration used to open the store is incompatible with the one that was used to create the store."
Here is a link to a GitHub repo with a dead-simple project with the reproducible issue: https://github.com/MatthewWaller/CoreDataTestingIssueNonBeta
Also, the relevant portion of my code is below. To make it work, add your xcdatamodel file with a "Note" entity with a String "title" attribute and add it to the test target.
import XCTest
import CoreData
#testable import CoreDataTestingIssue
class CoreDataTestingIssueTests: XCTestCase {
private var context: NSManagedObjectContext?
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
self.context = NSManagedObjectContext.contextForTests()
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExampleOne() throws {
guard let context = context else {
return
}
let note = Note(context: context)
note.title = "Hello"
try! context.save()
}
func testExampleTwo() throws {
guard let context = context else {
return
}
let note = Note(context: context)
note.title = "There"
try! context.save()
}
}
extension NSManagedObjectContext {
class func contextForTests() -> NSManagedObjectContext {
// Get the model
let model = NSManagedObjectModel.mergedModel(from: Bundle.allBundles)!
// Create and configure the coordinator
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
try! coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
// Setup the context
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = coordinator
return context
}
}
I have reproduced the fail. The problem is that your code implements this Note class twice.
In the File Inspector of .xcdatamodeld file, you should only choose one Target Membership.
Choose the application target if you need this data model in the app and tests, or choose the tests' target if you will only use it in your test code.
Hello I'm trying to unit testing a private function which located in Presenter
This is my Presenter Codes and I'm using Networking Singleton Object APIService
class MyPresenter {
weak var vc: MyProtocol?
func attachView(vc: MyProtocol?) {
self.vc = vc
}
func request(_ id: String) {
if id.count == 0 {
vc?.showIDEmptyAlert()
return
}
fetch(id)
}
private func fetch(_ id:String) {
DispatchQueue.global.async {
APIService.shared.fetch(id) { (data, err) in
if let err = err {
self.vc?.showErrorAlert()
return
}
self.vc?.update(data)
}
}
}
}
and this is my ViewController codes
class MyViewController: UIViewController, MyProtocol {
private var presenter: MyPresenter = MyPresenter()
override func viewDidLoad() {
super.viewDidLoad()
presenter.attachView(vc: self)
}
func showIDEmptyAlert() {
self.present ..
}
func showErrorAlert() {
self.present ..
}
func update(data: String) {
self.label.text = data
}
#IBAction func handleRegisterButton(_ sender: UIButton) {
guard let id = idTextField.text else { return }
presenter.request(id)
}
}
These are my Presenter and View. And I wrote Test Code like this
First, I made Mock PassiveView Like this
class MyViewMock: MyProtocol {
private (set) var showEmptyIdAlertHasBeenCalled = false
private (set) var showErrorAlertHasBeenCalled = false
private (set) var updateHasBeenCalled = false
func showEmptyIdAlert() {
showEmptyIdAlertHasBeenCalled = true
}
func showErrorAlert() {
showErrorAlertHasBeenCalled = true
}
func update(data: String) {
updateHasBeenCalled = true
}
}
So I expected that if I could test Presenter's request(_:) methods with valid id and invalid
but because request(_:) didn't get handler parameter and APIService.shared.fetch is asynchronous, I couldn't get
correct result by calling request(_:). (Always false)
How can I test this kind of Presenter?
In terms ofXCTests, there is the XCTestExpectation class to test asynchronous functions.
But there is an issue in your approach of testing the Presenter. You should use mock for your network service and stub it with expected arguments. It doesn't make sense to call the actual service. Unit tests are the fastest kind of tests. Private methods are a part of black-box which you should not care about its internal structure. Test public interfaces but don't test private methods.
If you will try to mock and stub APIService, then you notice it's impossible to do if it's a singleton. You'll end up with injecting service as a dependency into Presenter for the better testability.
If the service will be mocked and the stub will be used, then there is no need in using XCTestExpectation, because there won't be any asynchronous code.
To test asynchronous methods, you must use XCTestExpectation to make your test wait for the asynchronous operation to complete.
it's a test asynchronous method example
// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
// given
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Status code: 200")
// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
// then
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
// 2
promise.fulfill()
} else {
XCTFail("Status code: \(statusCode)")
}
}
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)
}
You can read more about Unit test from here
I watch realm tutorial from youtube, the instructor using singleton in the service class like the code below :
import Foundation
import RealmSwift
class RealmService {
// singleton
private init() {}
static let shared = RealmService()
var realm = try! Realm()
func save<T: Object>(object: T) {
do {
try realm.write {
realm.add(object)
}
} catch {
post(error)
}
}
func update<T: Object>(object: T, for dictionary: [String: Any?]) {
do {
try realm.write {
for (key,value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
func delete<T: Object>(object: T) {
do {
try realm.write {
realm.delete(object)
}
} catch {
post(error)
}
}
func post(_ error: Error) {
NotificationCenter.default.post(name: Notification.Name.realmError, object: error)
}
func observerRealmErrors(in vc: UIViewController, completion: #escaping(Error?) -> Void ) {
NotificationCenter.default.addObserver(forName: Notification.Name.realmError, object: nil, queue: nil) { (notification) in
completion(notification.object as? Error)
}
}
func stopObservingErrors(in vc: UIViewController) {
NotificationCenter.default.removeObserver(vc, name: NSNotification.Name.realmError, object: nil)
}
}
I understand that A singleton is an object which is instantiated exactly once. but why it needs to use singleton for this purpose? if the code s like below, I understand, because it doesn't make any sense if there is more than one "bobby" as the ID, then it makes sense to create a singleton class
but how about that realm service like the code above? why it needs to use singleton? I have seen some instructor using singleton for the service class
class AccountManager {
static let sharedInstance = AccountManager()
var userInfo = (ID: "Bobby", Password: 01036343984)
// Networking: communicating server
func network() {
// get everything
}
private init() { }
}
You gave an example of a shared property (userInfo) in your second block , but in the first block the instructor uses a singleton to prevent himself from duplicating code everywhere inside the project , and only to have one single unit to do the crud ( create , read , update , delete) operations of the Realm database and it's another use of a singleton class other than sharing a value
It's simple. If you don't try to using a singleton in realm case like you have wrote, you should call realm = try! Realm() method every time when you trying to use realm objects or functions(saving deleting etc..). It will make your code too boring and also can causes many miss point. If you using realm with the singleton code like you have wrote(it wrote with generic functions), you can save your object in realm database with just single line of code like RealmService.save(yourobject).
I use MagicalRecord for my project and in my database I have CDSong entity, which can be voted by multiple CDVoter entities.
The voters are added and deleted in background using NSManagedObjectContext.performAndWait(block:) called from a serial dispatch queue. I have an NSFetchedResultsController which fetches CDSongs and displays their voters (in this simple scenario it only prints the voters' names).
Everything would be fine, but I receive crashes occassionally in NSFetchedResultsControllerDelegate's controllerDidChangeContent method :-/ According to my analysis it seems like some invalid empty CDVoter (name = nil, votedSong = nil) objects appear in the CDSong.voters relationships. These empty voters are not returned from CDVoter.mr_findAll().
This is the code that simulates the crash (usually after <20 button clicks the app crashes because a CDVoter's name is nil). Am I doing something wrong with contexts and saving? Putting whole test code here with database and frc initialization if somebody wants to try it out, but the problematic part is in controllerDidChangeContent and buttonPressed methods. Thanks for your help :)
import UIKit
import CoreData
import MagicalRecord
class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate {
var frc: NSFetchedResultsController<NSFetchRequestResult>!
let dispatchQueue = DispatchQueue(label: "com.testQueue")
override func viewDidLoad() {
super.viewDidLoad()
self.initializeDatabase()
self.initializeFrc()
}
func initializeDatabase() {
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error)
MagicalRecord.setupCoreDataStack()
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn)
if CDSong.mr_findFirst() == nil {
for i in 1...5 {
let song = CDSong.mr_createEntity()!
song.id = Int16(i)
}
}
NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()
}
func initializeFrc() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil)
self.frc!.delegate = self
try! self.frc!.performFetch()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
for song in controller.fetchedObjects! {
print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! }))
}
print("----");
}
#IBAction func buttonPressed(_ sender: Any) {
for _ in 1...10 {
self.dispatchQueue.async {
let moc = NSManagedObjectContext.mr_()
moc.performAndWait {
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
moc.mr_saveToPersistentStoreAndWait()
}
}
}
}
}
Note:
I tried to use MagicalRecord.save(blockAndWait:) with no success.
Ok, so I found the reason of the crashes: Although mr_saveToPersistentStoreAndWait waits until the changes are saved into rootSavingContext, it doesn't wait until they are merged into defaultContext (if they were made by a private queue context). If the rootSavingContext has been changed by another save before the main queue context merges the old changes on main thread, the merge is then corrupted (changes in NSManagedObjectContextDidSave notification don't correspond to current context state of rootSavingContext in rootContextDidSave: internal method of MagicalRecord).
Explanation of my proposed solution:
DatabaseSavingManager contains a private queue saving context, which will be used for all saves in the application (maybe a drawback if you want to use multiple saving contexts, but it's enough for my needs - saves happen in background and consistency is maintained). As #Sneak commented, there's no reason to use a background serial queue which creates multiple contexts and waits for them to finish (that's what I originally did), because NSManagedObjectContext has its own serial queue, so now I used one context which is created on main thread and thus must be always called from main thread (using perform(block:) to avoid main thread blocking).
After saving to persistent store the saving context waits for the NSManagedObjectContextObjectsDidChange notification from defaultContext, so that it knows that the defaultContext has merged the changes. That's why no other saves than using DatabaseSavingManager's saving context are allowed, because they could confuse the waiting process.
Here is the code of DatabaseSavingManager:
import Foundation
import CoreData
class DatabaseSavingManager: NSObject {
static let shared = DatabaseSavingManager()
fileprivate let savingDispatchGroup = DispatchGroup()
fileprivate var savingDispatchGroupEntered = false
fileprivate lazy var savingContext: NSManagedObjectContext = {
if !Thread.current.isMainThread {
var context: NSManagedObjectContext!
DispatchQueue.main.sync {
context = NSManagedObjectContext.mr_()
}
return context
}
else {
return NSManagedObjectContext.mr_()
}
}()
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default())
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func save(block: #escaping (NSManagedObjectContext) -> ()) {
guard Thread.current.isMainThread else {
DispatchQueue.main.async {
self.save(block: block)
}
return
}
let moc = self.savingContext
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
func saveAndWait(block: #escaping (NSManagedObjectContext) -> ()) {
if Thread.current.isMainThread {
self.savingContext.performAndWait {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
else {
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
group.leave()
}
}
group.wait()
}
}
fileprivate func saveToPersistentStoreAndWait() {
if self.savingContext.hasChanges {
self.savingDispatchGroupEntered = true
self.savingDispatchGroup.enter()
self.savingContext.mr_saveToPersistentStoreAndWait()
self.savingDispatchGroup.wait()
}
}
#objc fileprivate func defaultContextDidUpdate(notification: NSNotification) {
if self.savingDispatchGroupEntered {
self.savingDispatchGroup.leave()
self.savingDispatchGroupEntered = false
}
}
}
And example how to use it (no NSFetchedResultsController crashes anymore; can be called from any thread, also very frequently):
DatabaseSavingManager.shared.save { (moc) in
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
}
Of course, this is not the most elegant solution for sure, just the first that came to my mind, so other approaches are welcome
Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.
I want to test that a tableView call to reloadData is executed on the main queue.
This should code should result in a passing test:
var cats = [Cat]() {
didSet {
DispatchQueue.main.async { [weak self] in
tableView.reloadData()
}
}
}
This code should result in a failing test:
var cats = [Cat]() {
didSet {
tableView.reloadData()
}
}
What should the test look like?
Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!
Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue
Then use dispatch_queue_get_specific to check for the presence of key & value:
fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1)
fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1)
/* Associate a key-value pair with the Main Queue */
dispatch_queue_set_specific(
dispatch_get_main_queue(),
mainQueueKey,
mainQueueValue,
nil
)
func isMainQueue() -> Bool {
/* Checking for presence of key-value on current queue */
return (dispatch_get_specific(mainQueueKey) == mainQueueValue)
}
I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()
fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)!
fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString)
extension UITableView {
var reloadDataCalledOnMainThread: Bool? {
get {
let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey)
return storedValue as? Bool
}
set {
objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
dynamic func _spyReloadData() {
reloadDataCalledOnMainThread = Thread.isMainThread
_spyReloadData()
}
//Then swizzle that with reloadData()
}
Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.
func testReloadDataIsCalledWhenCatsAreUpdated() {
// Checks for presence of another associated property that's set in the swizzled reloadData method
let reloadedPredicate = NSPredicate { [controller] _,_ in
controller.tableView.reloadDataWasCalled
}
expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil)
// Appends on the background queue to simulate an asynchronous call
DispatchQueue.global(qos: .background).async { [weak controller] in
let cat = Cat(name: "Test", identifier: 1)
controller?.cats.append(cat)
}
// 2 seconds seems excessive but NSPredicates only evaluate once per second
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(controller.tableView.reloadDataCalledOnMainThread!,
"Reload data should be called on the main thread when cats are updated on a background thread")
}
Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.
extension DispatchQueue {
func setAsExpectedQueue(isExpected: Bool = true) {
guard isExpected else {
setSpecific(key: .isExpectedQueueKey, value: nil)
return
}
setSpecific(key: .isExpectedQueueKey, value: true)
}
static func isExpectedQueue() -> Bool {
guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else {
return false
}
return isExpectedQueue
}
}
extension DispatchSpecificKey where T == Bool {
static let isExpectedQueueKey = DispatchSpecificKey<Bool>()
}
This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:
final class IsExpectedQueueTests: XCTestCase {
func testIsExpectedQueue() {
DispatchQueue.main.setAsExpectedQueue()
let valueExpectation = expectation(description: "The value was received on the expected queue")
let completionExpectation = expectation(description: "The publisher completed on the expected queue")
defer {
waitForExpectations(timeout: 1)
DispatchQueue.main.setAsExpectedQueue(isExpected: false)
}
DispatchQueue.global().sync {
Just(())
.receive(on: DispatchQueue.main)
.sink { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
completionExpectation.fulfill()
} receiveValue: { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
valueExpectation.fulfill()
}.store(in: &cancellables)
}
}
override func tearDown() {
cancellables.removeAll()
super.tearDown()
}
var cancellables = Set<AnyCancellable>()
}