Skip to content

Commit

Permalink
Inject Realm object as DataProvider in ReportViewModel methods
Browse files Browse the repository at this point in the history
This allows unit tests to pass when user is not signed in the app
  • Loading branch information
evnik committed Jan 2, 2021
1 parent bc3227c commit 555be01
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 17 deletions.
61 changes: 60 additions & 1 deletion O-FISHTests/MockDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,32 @@ import XCTest
@testable import O_FISH

class MockDataProvider: DataProvider {
private let onAdd: ((ObjectBase, Realm.UpdatePolicy) -> Void)?
private let onDelete: ((ObjectBase) -> Void)?
private let onDeleteResults: ((Any) -> Void)?
private let onObjects: ((Any.Type) -> Any)?
private let onWrite: (([NotificationToken]) throws -> Void)?

init(onDelete: ((ObjectBase) -> Void)? = nil) {
init(
onAdd: ((ObjectBase, Realm.UpdatePolicy) -> Void)? = nil,
onDelete: ((ObjectBase) -> Void)? = nil,
onDeleteResults: ((Any) -> Void)? = nil,
onObjects: ((Any.Type) -> Any)? = nil,
onWrite: (([NotificationToken]) throws -> Void)? = nil) {

self.onAdd = onAdd
self.onDelete = onDelete
self.onDeleteResults = onDeleteResults
self.onObjects = onObjects
self.onWrite = onWrite
}

func add(_ object: Object, update: Realm.UpdatePolicy) {
if let onAdd = self.onAdd {
onAdd(object, update)
} else {
XCTFail("Unexpected call of DataProvider.add(_:update:)")
}
}

func delete(_ object: ObjectBase) {
Expand All @@ -23,4 +45,41 @@ class MockDataProvider: DataProvider {
XCTFail("Unexpected call of DataProvider.delete(_:)")
}
}

func delete<Element: ObjectBase>(_ objects: Results<Element>) {
if let onDeleteResults = self.onDeleteResults {
onDeleteResults(objects)
} else {
XCTFail("Unexpected call of DataProvider.delete(_:)")
}
}

func objects<Element: Object>(_ elementType: Element.Type) -> Results<Element> {
if let onObjects = self.onObjects {
let result = onObjects(elementType)
if let castedResult = result as? Results<Element> {
return castedResult
} else {
XCTFail("Failed to cast \(type(of: result)) to \(Results<Element>.self)")
}
} else {
XCTFail("Unexpected call of DataProvider.objects(_:)")
}

fatalError("Failed to return a value")
}

func write<Result>(withoutNotifying tokens: [NotificationToken], _ block: () throws -> Result) throws -> Result {
if let onWrite = self.onWrite {
try onWrite(tokens)
return try block()
} else {
XCTFail("Unexpected call of DataProvider.write(withoutNotifying:_:)")
throw MockError.unexpectedCall
}
}

private enum MockError: Error {
case unexpectedCall
}
}
32 changes: 30 additions & 2 deletions O-FISHTests/ReportViewModelTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,37 @@ class ReportViewModelTest: XCTestCase {
guard let sut = sut else {
return
}
sut.save()

var writeCall = 0
let addExpectation = self.expectation(description: "DataProvider.add")
let deleteExpectation = self.expectation(description: "DataProvider.delete")
let writeExpectation = self.expectation(description: "DataProvider.write")
writeExpectation.expectedFulfillmentCount = 3
let dataProvider = MockDataProvider(
onAdd: { object, update in
addExpectation.fulfill()
XCTAssert(object is Report)
XCTAssertEqual(update, .error)
},
onDelete: { object in
deleteExpectation.fulfill()
XCTAssert(object is Report)
},
onWrite: { tokens in
writeCall += 1
writeExpectation.fulfill()
XCTAssertEqual(tokens, [])
if writeCall == 2 {
throw TestError()
}
})
sut.save(dataProvider)
XCTAssertNotNil(sut.report)
sut.discard()
sut.discard(dataProvider)
XCTAssertNil(sut.report)
self.waitForExpectations(timeout: 0.1)
}

private struct TestError: Error {
}
}
14 changes: 14 additions & 0 deletions o-fish-ios/ViewModel/DataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
import RealmSwift

protocol DataProvider {
func add(_ object: Object, update: Realm.UpdatePolicy)
func delete(_ object: ObjectBase)
func delete<Element: ObjectBase>(_ objects: Results<Element>)
func objects<Element: Object>(_ type: Element.Type) -> Results<Element>
func write<Result>(withoutNotifying tokens: [NotificationToken], _ block: () throws -> Result) throws -> Result
}

extension DataProvider {
func add(_ object: Object) {
self.add(object, update: .error)
}

func write<Result>(_ block: () throws -> Result) throws -> Result {
try self.write(withoutNotifying: [], block)
}
}

extension Realm: DataProvider {
Expand Down
2 changes: 1 addition & 1 deletion o-fish-ios/ViewModel/Report/PhotoViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class PhotoViewModel: ObservableObject, Identifiable {
}
}

static func delete(reportID: String, realm: Realm ) {
static func delete(reportID: String, realm: DataProvider) {
do {
let predicate = NSPredicate(format: "referencingReportID == %@", reportID)
try realm.write {
Expand Down
12 changes: 2 additions & 10 deletions o-fish-ios/ViewModel/Report/ReportViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,8 @@ class ReportViewModel: ObservableObject, Identifiable {
violationsWithCaptain.forEach { $0.crewMember = newCaptain }
}

func save() {
func save(_ realm: DataProvider) {
let isNewReport = (report == nil)
guard let realm = app.currentUser()?.agencyRealm() else {
print("Realm not available")
return
}
guard let report = isNewReport ? Report(id: id) : report else {
print("report not set")
return
Expand Down Expand Up @@ -108,11 +104,7 @@ class ReportViewModel: ObservableObject, Identifiable {
}
}

func discard() {
guard let realm = app.currentUser()?.agencyRealm() else {
print("Realm not available")
return
}
func discard(_ realm: DataProvider) {
PhotoViewModel.delete(reportID: id, realm: realm)
guard let report = report else {
print("Deleting, but report not set")
Expand Down
18 changes: 15 additions & 3 deletions o-fish-ios/Views/ReportFlow/ReportNavigationRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,20 @@ struct ReportNavigationRootView: View {

private func discardReport() {
let isDraft = report.draft
report.discard()
if let realm = app.currentUser()?.agencyRealm() {
self.report.discard(realm)
} else {
print("Realm not available")
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.showCanceledAlert(isDraft: isDraft)
}
}

private func saveAlertClicked() {
report.draft = true
report.save()
self.save()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.showSubmittedAlert(isDraft: true)
}
Expand All @@ -133,12 +138,19 @@ struct ReportNavigationRootView: View {
report.captain = captain
report.crew = [CrewMemberViewModel]()
}
report.save()
self.save()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.showSubmittedAlert(isDraft: wasDraft)
}
}

private func save() {
if let realm = app.currentUser()?.agencyRealm() {
self.report.save(realm)
} else {
print("Realm not available")
}
}
}

struct ReportNavigationRootView_Previews: PreviewProvider {
Expand Down

0 comments on commit 555be01

Please sign in to comment.