diff --git a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift index 465447a..aaffd76 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+CRUD.swift @@ -33,7 +33,7 @@ extension CoreDataRepository { object.create(from: item) let result: Result = .success(object) return result - .map(to: Model.RepoManaged.self, context: scratchPad) + .map(to: Model.RepoManaged.self) .save(context: scratchPad) .map(\.asUnmanaged) } @@ -55,7 +55,7 @@ extension CoreDataRepository { promise( Self.getObjectId(fromUrl: url, context: readContext) .mapToNSManagedObject(context: readContext) - .map(to: Model.RepoManaged.self, context: readContext) + .map(to: Model.RepoManaged.self) .map(\.asUnmanaged) ) } @@ -83,7 +83,7 @@ extension CoreDataRepository { scratchPad.transactionAuthor = transactionAuthor return Self.getObjectId(fromUrl: url, context: scratchPad) .mapToNSManagedObject(context: scratchPad) - .map(to: Model.RepoManaged.self, context: scratchPad) + .map(to: Model.RepoManaged.self) .map { repoManaged -> Model.RepoManaged in repoManaged.update(from: item) return repoManaged @@ -195,7 +195,7 @@ extension CoreDataRepository { readContext.performAndWait { let result = Self.getObjectId(fromUrl: url, context: readContext) .mapToNSManagedObject(context: readContext) - .map(to: T.self, context: readContext) + .map(to: T.self) promise(result) } }.eraseToAnyPublisher() diff --git a/Sources/CoreDataRepository/Result+CRUDHelpers.swift b/Sources/CoreDataRepository/Result+CRUDHelpers.swift index 6f8f581..a250234 100644 --- a/Sources/CoreDataRepository/Result+CRUDHelpers.swift +++ b/Sources/CoreDataRepository/Result+CRUDHelpers.swift @@ -26,7 +26,7 @@ extension Result where Success == NSManagedObjectID, Failure == CoreDataReposito } extension Result where Success == NSManagedObject, Failure == CoreDataRepositoryError { - func map(to _: T.Type, context _: NSManagedObjectContext) -> Result + func map(to _: T.Type) -> Result where T: RepositoryManagedModel { flatMap { _object in @@ -44,9 +44,15 @@ extension Result where Failure == CoreDataRepositoryError { do { try context.save() if let parentContext = context.parent { - try DispatchQueue.main.sync { - try parentContext.save() + var result: Result = .success(success) + parentContext.performAndWait { + do { + try parentContext.save() + } catch { + result = .failure(.coreData(error as NSError)) + } } + return result } return .success(success) } catch { diff --git a/Tests/CoreDataRepositoryTests/AggregateRepositoryTests.swift b/Tests/CoreDataRepositoryTests/AggregateRepositoryTests.swift index 980ec24..175692d 100644 --- a/Tests/CoreDataRepositoryTests/AggregateRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/AggregateRepositoryTests.swift @@ -34,25 +34,21 @@ final class AggregateRepositoryTests: CoreDataXCTestCase { Movie(id: UUID(), title: "E", releaseDate: Date(), boxOffice: 50), ] var objectIDs = [NSManagedObjectID]() - var _repository: CoreDataRepository? - var repository: CoreDataRepository { _repository! } override func setUpWithError() throws { try super.setUpWithError() - _repository = CoreDataRepository(context: viewContext) - objectIDs = movies.map { $0.asRepoManaged(in: self.viewContext).objectID } - try! viewContext.save() + objectIDs = try movies.map { $0.asRepoManaged(in: try self.viewContext()).objectID } + try viewContext().save() } override func tearDownWithError() throws { try super.tearDownWithError() - _repository = nil objectIDs = [] } func testCountSuccess() throws { let exp = expectation(description: "Get count of movies from CoreData") - let result: AnyPublisher<[[String: Int]], CoreDataRepositoryError> = repository + let result: AnyPublisher<[[String: Int]], CoreDataRepositoryError> = try repository() .count(predicate: NSPredicate(value: true), entityDesc: RepoMovie.entity()) var values: [[String: Int]] = [] result.subscribe(on: backgroundQueue) @@ -77,7 +73,7 @@ final class AggregateRepositoryTests: CoreDataXCTestCase { func testSumSuccess() throws { let exp = expectation(description: "Get sum of CoreData Movies boxOffice") var values: [[String: Decimal]] = [] - let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = repository.sum( + let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = try repository().sum( predicate: NSPredicate(value: true), entityDesc: RepoMovie.entity(), attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })! @@ -107,7 +103,7 @@ final class AggregateRepositoryTests: CoreDataXCTestCase { func testAverageSuccess() throws { let exp = expectation(description: "Get average of CoreData Movies boxOffice") var values: [[String: Decimal]] = [] - let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = repository.average( + let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = try repository().average( predicate: NSPredicate(value: true), entityDesc: RepoMovie.entity(), attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })! @@ -137,7 +133,7 @@ final class AggregateRepositoryTests: CoreDataXCTestCase { func testMinSuccess() throws { let exp = expectation(description: "Get average of CoreData Movies boxOffice") var values: [[String: Decimal]] = [] - let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = repository.min( + let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = try repository().min( predicate: NSPredicate(value: true), entityDesc: RepoMovie.entity(), attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })! @@ -167,7 +163,7 @@ final class AggregateRepositoryTests: CoreDataXCTestCase { func testMaxSuccess() throws { let exp = expectation(description: "Get average of CoreData Movies boxOffice") var values: [[String: Decimal]] = [] - let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = repository.max( + let result: AnyPublisher<[[String: Decimal]], CoreDataRepositoryError> = try repository().max( predicate: NSPredicate(value: true), entityDesc: RepoMovie.entity(), attributeDesc: RepoMovie.entity().attributesByName.values.first(where: { $0.name == "boxOffice" })! diff --git a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift index cb10545..24ce08f 100644 --- a/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/BatchRepositoryTests.swift @@ -49,22 +49,9 @@ final class BatchRepositoryTests: CoreDataXCTestCase { ] }() - var _repository: CoreDataRepository? - var repository: CoreDataRepository { _repository! } - - override func setUp() { - super.setUp() - _repository = CoreDataRepository(context: viewContext) - } - - override func tearDown() { - super.tearDown() - _repository = nil - } - func mapDictToRepoMovie(_ dict: [String: Any]) throws -> RepoMovie { try mapDictToMovie(dict) - .asRepoManaged(in: viewContext) + .asRepoManaged(in: try viewContext()) } func mapDictToMovie(_ dict: [String: Any]) throws -> Movie { @@ -76,12 +63,12 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testInsertSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let exp = expectation(description: "Successfully batch insert movies.") let request = NSBatchInsertRequest(entityName: try XCTUnwrap(RepoMovie.entity().name), objects: movies) - repository.insert(request) + try repository().insert(request) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -101,7 +88,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) XCTAssert( data.map { $0.title ?? "" }.sorted() == ["A", "B", "C", "D", "E"], "Inserted titles should match expectation" @@ -110,7 +97,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testInsertFailure() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let exp = expectation(description: "Fail to batch insert movies.") @@ -118,7 +105,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { entityName: try XCTUnwrap(RepoMovie.entity().name), objects: failureInsertMovies ) - repository.insert(request) + try repository().insert(request) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -136,18 +123,18 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) assert(data.map { $0.title ?? "" }.sorted() == [], "There should be no inserted values.") } func testCreateSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let exp = expectation(description: "Successfully batch insert movies.") let newMovies = try movies.map(mapDictToMovie(_:)) - let publisher: AnyPublisher<(success: [Movie], failed: [Movie]), Never> = repository.create(newMovies) + let publisher: AnyPublisher<(success: [Movie], failed: [Movie]), Never> = try repository().create(newMovies) publisher .subscribe(on: backgroundQueue) .receive(on: mainQueue) @@ -168,7 +155,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) XCTAssert( data.map { $0.title ?? "" }.sorted() == ["A", "B", "C", "D", "E"], "Inserted titles should match expectation" @@ -177,17 +164,17 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testReadSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let repoMovies = try movies .map(mapDictToRepoMovie(_:)) - try viewContext.save() + try viewContext().save() let exp = expectation(description: "Successfully batch update movies.") let urlsToRead = repoMovies.map(\.asUnmanaged).compactMap(\.url) var resultingMovies = [Movie]() - let publisher: AnyPublisher<(success: [Movie], failed: [URL]), Never> = repository.read(urls: urlsToRead) + let publisher: AnyPublisher<(success: [Movie], failed: [URL]), Never> = try repository().read(urls: urlsToRead) publisher .subscribe(on: backgroundQueue) .receive(on: mainQueue) @@ -215,19 +202,19 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testUpdateSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") _ = try movies .map(mapDictToRepoMovie(_:)) - try viewContext.save() + try viewContext().save() let exp = expectation(description: "Successfully batch update movies.") let predicate = NSPredicate(value: true) let request = NSBatchUpdateRequest(entityName: try XCTUnwrap(RepoMovie.entity().name)) request.predicate = predicate request.propertiesToUpdate = ["title": "Updated!", "boxOffice": 1] - repository.update(request) + try repository().update(request) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -247,7 +234,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) XCTAssert( data.map { $0.title ?? "" }.sorted() == ["Updated!", "Updated!", "Updated!", "Updated!", "Updated!"], "Updated titles should match request" @@ -256,19 +243,19 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testAltUpdateSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let repoMovies = try movies .map(mapDictToRepoMovie(_:)) - try viewContext.save() + try viewContext().save() let exp = expectation(description: "Successfully batch update movies.") var editedMovies = repoMovies.map(\.asUnmanaged) let newTitles = ["ZA", "ZB", "ZC", "ZD", "ZE"] var resultingMovies = [Movie]() newTitles.enumerated().forEach { index, title in editedMovies[index].title = title } - repository.update(editedMovies) + try repository().update(editedMovies) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -295,12 +282,12 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testDeleteSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") _ = try movies .map(mapDictToRepoMovie(_:)) - try viewContext.save() + try viewContext().save() let exp = expectation(description: "Successfully batch delete movies.") let request = @@ -308,7 +295,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { RepoMovie .entity().name ))) - repository.delete(request) + try repository().delete(request) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -327,9 +314,9 @@ final class BatchRepositoryTests: CoreDataXCTestCase { ) .store(in: &cancellables) wait(for: [exp], timeout: 5) - viewContext.reset() + try viewContext().reset() - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) XCTAssert(data.map { $0.title ?? "" }.sorted() == [], "There should be no remaining values.") } @@ -337,16 +324,16 @@ final class BatchRepositoryTests: CoreDataXCTestCase { func testAltDeleteSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let repoMovies = try movies .map(mapDictToRepoMovie(_:)) - try viewContext.save() + try viewContext().save() let exp = expectation(description: "Successfully batch update movies.") let urlsToDelete = repoMovies.map(\.asUnmanaged).compactMap(\.url) - repository.delete(urls: urlsToDelete) + try repository().delete(urls: urlsToDelete) .subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -367,7 +354,7 @@ final class BatchRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let data = try viewContext.fetch(fetchRequest) + let data = try viewContext().fetch(fetchRequest) XCTAssert(data.map { $0.title ?? "" }.sorted() == [], "There should be no remaining values.") } } diff --git a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift index 73a6f87..e2c8958 100644 --- a/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/CRUDRepositoryTests.swift @@ -24,27 +24,14 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { ("testReadSubscriptionSuccess", testReadSubscriptionSuccess), ] - var _repository: CoreDataRepository? - var repository: CoreDataRepository { _repository! } - - override func setUp() { - super.setUp() - _repository = CoreDataRepository(context: viewContext) - } - - override func tearDown() { - super.tearDown() - _repository = nil - } - func testCreateSuccess() throws { let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") let exp = expectation(description: "Create a RepoMovie in CoreData") var movie = Movie(id: UUID(), title: "Create Success", releaseDate: Date(), boxOffice: 100) - repository.create(movie).subscribe(on: backgroundQueue) + try repository().create(movie).subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( receiveCompletion: { completion in @@ -67,7 +54,7 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { ) .store(in: &cancellables) wait(for: [exp], timeout: 5) - let all = ((try? viewContext.fetch(RepoMovie.fetchRequest())) ?? []).map(\.asUnmanaged) + let all = (try viewContext().fetch(RepoMovie.fetchRequest())).map(\.asUnmanaged) XCTAssert( all.count == 1, "There should be only one CoreData object after creating one, but found \(all.count)." @@ -82,16 +69,16 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testReadSuccess() throws { var movie = Movie(id: UUID(), title: "Read Success", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: RepoMovie.fetchRequest()) + let countAfterCreate = try viewContext().count(for: RepoMovie.fetchRequest()) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") let exp = expectation(description: "Read a RepoMovie in CoreData") - let result: AnyPublisher = repository.read(try XCTUnwrap(movie.url)) + let result: AnyPublisher = try repository().read(try XCTUnwrap(movie.url)) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -117,19 +104,19 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testReadFailure() throws { var movie = Movie(id: UUID(), title: "Read Failure", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: RepoMovie.fetchRequest()) + let countAfterCreate = try viewContext().count(for: RepoMovie.fetchRequest()) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") - viewContext.delete(repoMovie) - try? viewContext.save() + try viewContext().delete(repoMovie) + try viewContext().save() let exp = expectation(description: "Fail to read a RepoMovie in CoreData") - let result: AnyPublisher = repository.read(try XCTUnwrap(movie.url)) + let result: AnyPublisher = try repository().read(try XCTUnwrap(movie.url)) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -153,18 +140,18 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testUpdateSuccess() throws { var movie = Movie(id: UUID(), title: "Update Success", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try viewContext.count(for: RepoMovie.fetchRequest()) + let countAfterCreate = try viewContext().count(for: RepoMovie.fetchRequest()) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") movie.title = "Update Success - Edited" let exp = expectation(description: "Update a RepoMovie in CoreData") - let result: AnyPublisher = repository + let result: AnyPublisher = try repository() .update(try XCTUnwrap(movie.url), with: movie) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) @@ -187,10 +174,10 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { wait(for: [exp], timeout: 10) let objectId = try XCTUnwrap( - viewContext.persistentStoreCoordinator? + viewContext().persistentStoreCoordinator? .managedObjectID(forURIRepresentation: try XCTUnwrap(movie.url)) ) - let updatedRepoMovie = try viewContext.existingObject(with: objectId) + let updatedRepoMovie = try viewContext().existingObject(with: objectId) let updatedMovie = try XCTUnwrap(updatedRepoMovie as? RepoMovie).asUnmanaged let diff = CustomDump.diff(updatedMovie, movie) XCTAssertNil(diff, "CoreData movie should be updated with the new title, but found diff \(diff ?? "").") @@ -199,22 +186,22 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testUpdateFailure() throws { var movie = Movie(id: UUID(), title: "Update Failure", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: fetchRequest) + let countAfterCreate = try viewContext().count(for: fetchRequest) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") - viewContext.delete(repoMovie) - try viewContext.save() + try viewContext().delete(repoMovie) + try viewContext().save() - let countAfterDelete = try? viewContext.count(for: fetchRequest) + let countAfterDelete = try viewContext().count(for: fetchRequest) XCTAssert(countAfterDelete == 0, "Count of objects in CoreData should be 0 after delete for read test.") let exp = expectation(description: "Fail to update a RepoMovie in CoreData") - let result: AnyPublisher = repository + let result: AnyPublisher = try repository() .update(try XCTUnwrap(movie.url), with: movie) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) @@ -239,16 +226,16 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testDeleteSuccess() throws { var movie = Movie(id: UUID(), title: "Delete Success", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: RepoMovie.fetchRequest()) + let countAfterCreate = try viewContext().count(for: RepoMovie.fetchRequest()) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") let exp = expectation(description: "Delete a RepoMovie in CoreData") - let result: AnyPublisher = repository.delete(try XCTUnwrap(movie.url)) + let result: AnyPublisher = try repository().delete(try XCTUnwrap(movie.url)) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -268,26 +255,26 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { .store(in: &cancellables) wait(for: [exp], timeout: 5) - let afterDeleteCount = try viewContext.count(for: fetchRequest) + let afterDeleteCount = try viewContext().count(for: fetchRequest) XCTAssert(afterDeleteCount == 0, "CoreData should have no objects after delete but found \(afterDeleteCount)") } func testDeleteFailure() throws { var movie = Movie(id: UUID(), title: "Delete Failure", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: fetchRequest) + let countAfterCreate = try viewContext().count(for: fetchRequest) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for delete test.") - viewContext.delete(repoMovie) - try? viewContext.save() + try viewContext().delete(repoMovie) + try viewContext().save() let exp = expectation(description: "Fail to delete a RepoMovie in CoreData") - let result: AnyPublisher = repository.delete(try XCTUnwrap(movie.url)) + let result: AnyPublisher = try repository().delete(try XCTUnwrap(movie.url)) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink( @@ -311,12 +298,12 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { func testReadSubscriptionSuccess() throws { var movie = Movie(id: UUID(), title: "Read Success", releaseDate: Date(), boxOffice: 100) let fetchRequest = NSFetchRequest(entityName: "RepoMovie") - let count = try? viewContext.count(for: fetchRequest) + let count = try viewContext().count(for: fetchRequest) XCTAssert(count == 0, "Count of objects in CoreData should be zero at the start of each test.") - let repoMovie = movie.asRepoManaged(in: viewContext) - try? viewContext.save() + let repoMovie = movie.asRepoManaged(in: try viewContext()) + try viewContext().save() movie.url = repoMovie.objectID.uriRepresentation() - let countAfterCreate = try? viewContext.count(for: RepoMovie.fetchRequest()) + let countAfterCreate = try viewContext().count(for: RepoMovie.fetchRequest()) XCTAssert(countAfterCreate == 1, "Count of objects in CoreData should be 1 for read test.") var editedMovie = movie @@ -325,7 +312,8 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { let firstExp = expectation(description: "Read a movie from CoreData") let secondExp = expectation(description: "Read a movie again after CoreData context is updated") var resultCount = 0 - let result: AnyPublisher = repository.readSubscription(try XCTUnwrap(movie.url)) + let result: AnyPublisher = try repository() + .readSubscription(try XCTUnwrap(movie.url)) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink(receiveCompletion: { completion in @@ -351,7 +339,7 @@ final class CRUDRepositoryTests: CoreDataXCTestCase { }) .store(in: &cancellables) wait(for: [firstExp], timeout: 5) - repository.update(try XCTUnwrap(movie.url), with: editedMovie).sink( + try repository().update(try XCTUnwrap(movie.url), with: editedMovie).sink( receiveCompletion: { completion in if case .failure = completion { XCTFail("Update should not fail") diff --git a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift index 60184a4..1ef9b6e 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataXCTestCase.swift @@ -13,22 +13,48 @@ import XCTest class CoreDataXCTestCase: XCTestCase { var cancellables: Set = [] + var _container: NSPersistentContainer? var _viewContext: NSManagedObjectContext? + var _repositoryContext: NSManagedObjectContext? + var _repository: CoreDataRepository? let mainQueue = DispatchQueue.main let backgroundQueue = DispatchQueue(label: "background", qos: .userInitiated) - var viewContext: NSManagedObjectContext { _viewContext! } + func container() throws -> NSPersistentContainer { + try XCTUnwrap(_container) + } + + func viewContext() throws -> NSManagedObjectContext { + try XCTUnwrap(_viewContext) + } + + func repositoryContext() throws -> NSManagedObjectContext { + try XCTUnwrap(_repositoryContext) + } + + func repository() throws -> CoreDataRepository { + try XCTUnwrap(_repository) + } override func setUpWithError() throws { let container = CoreDataStack.persistentContainer + _container = container _viewContext = container.viewContext _viewContext?.automaticallyMergesChangesFromParent = true + backgroundQueue.sync { + _repositoryContext = container.newBackgroundContext() + _repositoryContext?.automaticallyMergesChangesFromParent = true + } + _repository = CoreDataRepository(context: try repositoryContext()) try super.setUpWithError() } override func tearDownWithError() throws { try super.tearDownWithError() + _container = nil _viewContext = nil + _repositoryContext = nil + _repository = nil cancellables.forEach { $0.cancel() } } } diff --git a/Tests/CoreDataRepositoryTests/FetchRepositoryTests.swift b/Tests/CoreDataRepositoryTests/FetchRepositoryTests.swift index 0b1d8f2..721dff6 100644 --- a/Tests/CoreDataRepositoryTests/FetchRepositoryTests.swift +++ b/Tests/CoreDataRepositoryTests/FetchRepositoryTests.swift @@ -31,26 +31,28 @@ final class FetchRepositoryTests: CoreDataXCTestCase { Movie(id: UUID(), title: "E", releaseDate: Date()), ] var expectedMovies = [Movie]() - var _repository: CoreDataRepository? - var repository: CoreDataRepository { _repository! } override func setUpWithError() throws { try super.setUpWithError() - _repository = CoreDataRepository(context: viewContext) - _ = movies.map { $0.asRepoManaged(in: viewContext) } - try viewContext.save() - expectedMovies = try viewContext.fetch(fetchRequest).map(\.asUnmanaged) + try repositoryContext().performAndWait { + do { + _ = try movies.map { $0.asRepoManaged(in: try repositoryContext()) } + try repositoryContext().save() + expectedMovies = try repositoryContext().fetch(fetchRequest).map(\.asUnmanaged) + } catch { + XCTFail("Failed to setup context") + } + } } override func tearDownWithError() throws { try super.tearDownWithError() - _repository = nil expectedMovies = [] } func testFetchSuccess() throws { let exp = expectation(description: "Fetch movies from CoreData") - let result: AnyPublisher<[Movie], CoreDataRepositoryError> = repository.fetch(fetchRequest) + let result: AnyPublisher<[Movie], CoreDataRepositoryError> = try repository().fetch(fetchRequest) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink(receiveCompletion: { completion in @@ -72,7 +74,7 @@ final class FetchRepositoryTests: CoreDataXCTestCase { let firstExp = expectation(description: "Fetch movies from CoreData") let secondExp = expectation(description: "Fetch movies again after CoreData context is updated") var resultCount = 0 - let result: AnyPublisher<[Movie], CoreDataRepositoryError> = repository.fetchSubscription(fetchRequest) + let result: AnyPublisher<[Movie], CoreDataRepositoryError> = try repository().fetchSubscription(fetchRequest) result.subscribe(on: backgroundQueue) .receive(on: mainQueue) .sink(receiveCompletion: { completion in @@ -100,9 +102,16 @@ final class FetchRepositoryTests: CoreDataXCTestCase { }) .store(in: &cancellables) wait(for: [firstExp], timeout: 5) - let crudRepository = CoreDataRepository(context: viewContext) - let _: AnyPublisher = crudRepository - .delete(try XCTUnwrap(expectedMovies.last?.url)) + try repositoryContext().performAndWait { + do { + let objectId = try container().persistentStoreCoordinator + .managedObjectID(forURIRepresentation: try XCTUnwrap(expectedMovies.last?.url)) + try repositoryContext().delete(try repositoryContext().object(with: try XCTUnwrap(objectId))) + try repositoryContext().save() + } catch { + XCTFail("Failed to update repository: \(error.localizedDescription)") + } + } wait(for: [secondExp], timeout: 5) } }