I've got this simple class to store and instance of the type CurrentUser to Realm called CurrentUserStore. There is one rule in this class, that is there can only be one record for an object at all times so for the register(user:) func will need to clear the Realm for that type first before adding a new user. Here's the code:
final class CurrentUserStore: CurrentUserStoreProtocol {
private let realmAdapter: RealmAdapterInterface
init(realmAdapter: RealmAdapterInterface) {
self.realmAdapter = realmAdapter
}
func initiate() throws { ... }
func register(user: User) {
if !realmAdapter.retrieve(type: User.Type).isEmpty {
realmAdapter.clear(type: User.Type)
}
realmAdapter.add(user)
}
func retrieve() -> User { ... }
func unregister() { ... }
}
protocol RealmAdapterInterface {
func initiate() throws
func add<T: Any>(_ entry: T)
func retrieve<T: Any>(type: T.Type) -> [T]
func clear<T: Any>(type: T.Type)
}
Now, to test this behaviour, I have this:
class CurrentUserStoreTest: XCTestCase {
func testRegister_WhenAdapterRetrievesTooManyUsers_ShouldClearAdapterFirst() {
let realmAdapter = MockRealmAdapter(retrieveReturnValue: [User.random(), User.random()])
let store = CurrentUserStore(realmAdapter: realmAdapter)
let inputtedUser = User.random()
store.register(inputtedUser)
XCTAssertTrue(realmAdapter.isClearCalled)
XCTAssertTrue(realmAdapter.clearArgumentType is User.Type)
XCTAssertTrue(realmAdapter.isAddCalled)
XCTAssertEqual(inputtedUser, realmAdapter.addArgumentUser)
}
}
private final class MockRealmAdapter: RealmAdapterInterface {
private(set) var isInitiateCalled = false
private(set) var isAddCalled = false
private(set) var isRetrieveCalled = false
private(set) var isClearCalled = false
private(set) var initiateCallCount = 0
private(set) var addArgumentUser: Any?
private(set) var retrieveArgumentType: Any.Type?
private(set) var clearArgumentType: Any.Type?
private let retrieveReturnValue: [Any]
init(retrieveReturnValue: [Any] = []) {
self.retrieveReturnValue = retrieveReturnValue
}
func initiate() throws {
isInitiateCalled = true
initiateCallCount += 1
}
func add<T>(_ entry: T) throws {
isAddCalled = true
addArgumentUser = entry
}
func retrieve<T>(type: T.Type) throws -> [T] {
isRetrieveCalled = true
retrieveArgumentType = type
return retrieveReturnValue
}
func clear<T>(ype: T.Type) throws {
isClearCalled = true
clearArgumentType = type
}
}
Now, if I were to make the register(user:) func into this:
func register(user: User) {
if !realmAdapter.retrieve(type: User.Type).isEmpty {
realmAdapter.add(user)
realmAdapter.clear(type: User.Type)
} else {
realmAdapter.add(user)
}
}
The test will still be green because we will still call add(_:) and clear(type:) funcs of the adapter class. Although because of the order of execution, the result will be very different. Any idea about how to test this order of execution?
Thanks.
So, after I've got some revelations from the comment by #matt, I've changed my mock class to this:
class CurrentUserStoreTest: XCTestCase {
func testRegister_WhenAdapterRetrievesTooManyUsers_ShouldClearAdapterFirst() {
let realmAdapter = MockRealmAdapter(retrieveReturnValue: [User.random(), User.random()])
let store = CurrentUserStore(realmAdapter: realmAdapter)
let inputtedUser = User.random()
store.register(inputtedUser)
XCTAssertTrue(realmAdapter.isClearCalled)
XCTAssertTrue(realmAdapter.clearArgumentType is User.Type)
XCTAssertTrue(realmAdapter.isAddCalled)
XCTAssertEqual(inputtedUser, realmAdapter.addArgumentUser)
XCTAssertEqual([.retrieveCalled, .clearCalled, .addCalled], realmAdapter.functionCallOrder)
}
}
enum RealmAdapterFunctionCall {
case initiateCalled
case addCalled
case retrieveCalled
case clearCalled
}
private final class MockRealmAdapter: RealmAdapterInterface {
private(set) var functionCallOrder: [RealmAdapterFunctionCall] = []
private(set) var isInitiateCalled = false
private(set) var isAddCalled = false
private(set) var isRetrieveCalled = false
private(set) var isClearCalled = false
private(set) var initiateCallCount = 0
private(set) var addArgumentUser: Any?
private(set) var retrieveArgumentType: Any.Type?
private(set) var clearArgumentType: Any.Type?
private let retrieveReturnValue: [Any]
init(retrieveReturnValue: [Any] = []) {
self.retrieveReturnValue = retrieveReturnValue
}
func initiate() throws {
isInitiateCalled = true
functionCallOrder.append(.initiateCalled)
initiateCallCount += 1
}
func add<T>(_ entry: T) throws {
isAddCalled = true
functionCallOrder.append(.addCalled)
addArgumentUser = entry
}
func retrieve<T>(type: T.Type) throws -> [T] {
isRetrieveCalled = true
functionCallOrder.append(.retrieveCalled)
retrieveArgumentType = type
return retrieveReturnValue
}
func clear<T>(ype: T.Type) throws {
isClearCalled = true
functionCallOrder.append(.clearCalled)
clearArgumentType = type
}
}
Related
My presenter class MyItemsPresenter will look something like so:
class MyItemsPresenter {
internal var deliveryType: ServiceType = .typeA
var myItemSections: [MySections] = [.sectionA, .sectionB, .sectionC]
var didMoveDeals = false
private(set) var childViewControllers = [UIViewController]()
var segmentTitles: [String] {
myItemSections.map { $0.title }
}
var selectedOption: String?
func createChildViewControllers() -> [UIViewController] {
var childViewControllers = [UIViewController]()
for item in myItemSections {
switch item {
case . sectionA:
childViewControllers.append(MyListWireframe.assembleModuleA())
case . sectionB:
childViewControllers.append(MyListWireframe.assembleModuleB())
case . sectionC:
childViewControllers.append(MyListWireframe.assembleModuleC())
default:
break
}
}
self.childViewControllers = childViewControllers
return childViewControllers
}
private func resetSections() {
myItemSections = getOriginalSections()
}
private func getOriginalSections() -> [MySections] {
[.list, .buyItAgain, .clippedDeals, .lastOrder]
}
func showFirstTwoMenuItems() {
if myItemSections.count > 2 {
let upperRange = myItemSections.count-1
myItemSections.removeSubrange(ClosedRange(uncheckedBounds: (lower: 2, upper: upperRange)))
childViewControllers.removeSubrange(ClosedRange(uncheckedBounds: (lower: 2, upper: upperRange)))
}
}
}
For writing test cases, I would like to write a Mock Presenter class for the above class say MyItemsPresenterMock. So how will my mock presenter class look? I'm using Quick and Nimble for test cases.
I wrote some tests for my ViewModel. I use RxSwift in this project. I have never before write unit tests, so i want to ask you about correctness of them. What can I do better next time? It is little difficult for me when I write tests while I use RxSwift. All tests passed, but I don't know if they are "good tests". Thanks for your help.
ViewModel:
class SettingsViewModel {
private let storage = Storage.shared
private let disposeBag = DisposeBag()
let userSettings = BehaviorRelay<UserSettings>(value: UserSettings(name: "", tags: []))
init() {
subscribe()
}
private func subscribe() {
storage.currentUserSettings()
.subscribe(onNext: { settings in
if let settings = settings {
self.userSettings.accept(settings)
}
})
.disposed(by: disposeBag)
}
func saveName(_ name: String) {
saveSettings(name: name, tags: userSettings.value.tags)
}
func addTag(_ tag: String) {
let newTags = userSettings.value.tags + [tag]
saveSettings(name: userSettings.value.name, tags: newTags)
}
func removeTag(_ index: Int) {
var newTags = userSettings.value.tags
newTags.remove(at: index)
saveSettings(name: userSettings.value.name, tags: newTags)
}
private func saveSettings(name: String, tags: [String]) {
let newSettings = UserSettings(name: name, tags: tags)
Storage.shared.saveUserSettings(newSettings)
}
}
Test class:
class SettingsViewModelTests: XCTestCase {
func test_userSettingsSaving_includesAddingName() {
let sut = SettingsViewModel()
let userSettings = UserSettingsSpy(sut.userSettings)
sut.saveName("George")
XCTAssertEqual(userSettings.settings.name, "George")
sut.saveName("Mike")
XCTAssertEqual(userSettings.settings.name, "Mike")
}
func test_userSettingsSaving_includesAddingTag() {
let sut = SettingsViewModel()
let userSettings = UserSettingsSpy(sut.userSettings)
sut.addTag("Book")
var savedTags: [String] = []
Storage.shared.currentUserSettings()
.subscribe(onNext: { settings in
if let tags = settings?.tags {
savedTags = tags
}
})
.dispose()
XCTAssertEqual(userSettings.settings.tags, savedTags)
}
func test_userSettingsSaving_includesRemovingTag() {
let sut = SettingsViewModel()
let userSettings = UserSettingsSpy(sut.userSettings)
sut.addTag("TestTagToRemove")
sut.removeTag(0)
var savedTags: [String] = []
Storage.shared.currentUserSettings()
.subscribe(onNext: { settings in
if let tags = settings?.tags {
savedTags = tags
}
})
.dispose()
XCTAssertEqual(userSettings.settings.tags, savedTags)
}
class UserSettingsSpy {
private let disposeBag = DisposeBag()
private(set) var settings = UserSettings(name: "", tags: [])
init(_ observable: BehaviorRelay<UserSettings>) {
observable
.subscribe(onNext: { settings in
self.settings = settings
})
.disposed(by: disposeBag)
}
}
}
An easy way to check the correctness of your tests is to change the system under test and see if your tests flag the error. If they don't, then that is a hole in your tests. For example, the following view model will pass your tests:
struct Storage {
static let shared = Storage()
func currentUserSettings() -> Observable<UserSettings?> { .just(nil) }
}
struct SettingsViewModel {
let userSettings = BehaviorRelay<UserSettings>(value: UserSettings())
func saveName(_ value: String) {
userSettings.accept(UserSettings(name: value, tags: []))
}
func addTag(_ value: String) { }
func removeTag(_ value: Int) { }
}
struct UserSettings {
var name: String = ""
var tags: [String] = []
}
The code above is obviously missing some important functionality which means your tests are incomplete.
I'm trying to pass data between viewControllers, but something seems wrong.
The first viewController I want to set the "Bool" to the protocol function to be able to recover in the other screen. What am I doing wrong, I always used protocols but at this time I got in trouble.
That's how I'm doing that:
//
// ComboBoxNode.swift
//
import Foundation
import SWXMLHash
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool)
}
class ComboBoxNode: FormControlNode, IFormControlDataSource {
var listType: String?
var dataSource: String?
var dataSourceValue: String?
var dataSourceText: String?
var hasCustomOption:Bool?
var customOptionText: String?
var ctrlDataSourceType: String?
var parameters = [ParameterNode]()
var staticList: FormControlStaticListNode?
var delegate:ComboBoxNodeDelegate?
override init(indexer: XMLIndexer) {
super.init(indexer: indexer)
guard let element = indexer.element else {
preconditionFailure("Error")
}
let isCustomOption = element.bool(by: .hasCustomOption) ?? hasCustomOption
if isCustomOption == true {
self.delegate?.getCustomOption(data: hasCustomOption!)
}
self.readFormControlDataSource(indexer: indexer)
}
override func accept<T, E: IViewVisitor>(visitor: E) -> T where E.T == T {
return visitor.visit(node: self)
}
}
That's how I'm trying to recover on next screen:
// FormPickerViewDelegate.swift
import Foundation
import ViewLib
import RxSwift
class FormPickerViewDelegate: NSObject {
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
private var controlViewModel: FormControlViewModel
private var customText:Bool?
private var PickerNodeDelegate:ComboBoxNodeDelegate?
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
}
func getItemByValue(_ value: Any) -> (AnyHashable, String)? {
if value is AnyHashable {
let found = items.value.filter {$0.value == value as! AnyHashable}
if found.count >= 1 {
return found[0]
}
}
return nil
}
}
extension FormPickerViewDelegate:ComboBoxNodeDelegate {
func getCustomOption(data: Bool) {
customText = data
}
}
Instead of setting PickerNodeDelegate = self in didSet {} closure
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
Assign it in your init() function instead
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
PickerNodeDelegate = self
}
Note, your should declare your delegate to be weak also, since it's a delegate, your protocol should conform to be a class type in order to be weakified.
protocol ComboBoxNodeDelegate: class
...
weak var delegate: ComboBoxNodeDelegate?
Here is an example, hope it helps!
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool) -> String
}
class ViewOne:ComboBoxNodeDelegate {
var foo:Bool = false
var bar:String = "it works!"
/** Return: String */
func getCustomOption(data:Bool) -> String { //conform here to protocol
// do whatever you wanna do here ...example
self.foo = data // you can set
return bar // even return what you want
}
//initialize
func initalizeViewTwo() {
let v2 = ViewTwo()
v2.delegate = self //since `self` conforms to the ComboBoxNodeDelegate protcol you are allowed to set
}
}
class ViewTwo {
var delegate:ComboBoxNodeDelegate?
func getCustomOption_forV1() {
let view2_foo = delegate.getCustomOption(data:true)
print(view2_foo) // should print "it works!"
}
}
All parameters passed around in Swift are constants -- so you cannot change them.
If you want to change them in a function, you must declare your protocol to pass by reference with inout:
protocol ComboBoxNodeDelegate {
func getCustomOption(data: inout Bool)
}
Note: you cannot pass a constant (let) to this function. It must be a variable -- which I see you are doing!
Any ideas why Swift is not smart enough to infer the parameters passed to the observeWrapper function.
Code:
let implementation = QuestionJSONStrategy(name: questionGroup.course.rawValue)
_ = observeWrapper(implementation)
showQuestion()
}
func observeWrapper<T: NSObject & QuestionStrategy>(_ object: T) -> NSKeyValueObservation {
return object.observe(\.questionIndex, options: .new) { _, change in
guard let newValue = change.newValue else { return }
print(newValue)
}
}
QuestionStrategy Protocol:
#objc protocol QuestionStrategy :AnyObject {
var questions :[Question] { get set}
var questionIndex :Int { get set }
init(name :String)
func nextQuestion() -> Question
}
QuestionJSONStrategy Class:
#objc public class QuestionJSONStrategy :NSObject, QuestionStrategy {
var questions: [Question] = [Question]()
#objc dynamic var questionIndex: Int = 0
I am facing an issue where I am unable to keep existing relationships after calling add(_, update: true) function.
I wrote a TaskSync class that is responsible for creating/updating Task objects:
class TaskSync: ISync {
typealias Model = Task
func sync(model: Task) {
let realm = try! Realm()
let inWrite = realm.isInWriteTransaction
if !inWrite {
realm.beginWrite()
}
let _task = realm.object(ofType: Task.self, forPrimaryKey: model.id)
// Persist matches as they are not getting fetched with the task
if let _task = _task {
print("matches: \(_task.matches.count)")
model.matches = _task.matches
}
realm.add(model, update: true)
if _task == nil {
var user = realm.object(ofType: User.self, forPrimaryKey: model.getUser().id)
if (user == nil) {
user = model.getUser()
realm.add(user!, update: true)
}
user!.tasks.append(model)
}
if !inWrite {
try! realm.commitWrite()
}
}
func sync(models: List<Task>) {
let realm = try! Realm()
try! realm.write {
models.forEach { task in
sync(model: task)
}
}
}
}
When a model is to be synced, I check if it already exists in the Realm and if so, I fetch it and try to include the matches property as this one is not included in the model.
Right before the call realm.add(model, update: true), model contains list of matches, however right after the realm.add is executed, the matches list is empty.
Here are the two models:
class Task: Object, ElementPreloadable, ElementImagePreloadable, ItemSectionable {
dynamic var id: Int = 0
dynamic var title: String = ""
dynamic var desc: String = ""
dynamic var price: Float = 0.0
dynamic var calculatedPrice: Float = 0.0
dynamic var location: String = ""
dynamic var duration: Int = 0
dynamic var date: String = ""
dynamic var category: Category?
dynamic var currency: Currency?
dynamic var longitude: Double = 0.0
dynamic var latitude: Double = 0.0
dynamic var state: Int = 0
dynamic var userId: Int = 0
// Existing images
var imagesExisting = List<URLImage>()
// New images
var imagesNew = List<Image>()
// Images deleted
var imagesDeleted = List<URLImage>()
private let users = LinkingObjects(fromType: User.self, property: "tasks")
var user: User?
var matches = List<Match>()
dynamic var notification: Notification?
override static func ignoredProperties() -> [String] {
return ["imagesExisting", "imagesNew", "imagesDeleted", "user", "tmpUser"]
}
override static func primaryKey() -> String? {
return "id"
}
func getImageMain() -> URLImage? {
for image in imagesExisting {
if image.main {
return image
}
}
return imagesExisting.first
}
func getSection() -> Int {
return state
}
func getSectionFieldName() -> String? {
return "state"
}
func getId() -> Int {
return id
}
func getURL() -> URL? {
if let image = getImageMain() {
return image.getResizedURL()
}
return nil
}
func getState() -> TaskOwnState {
return TaskOwnState(rawValue: state)!
}
func getUser() -> User {
return (user != nil ? user : users.first)!
}
}
class Match: Object, ElementPreloadable, ElementImagePreloadable, ItemSectionable {
dynamic var id: Int = 0
dynamic var state: Int = -1
dynamic var priorityOwnRaw: Int = 0
dynamic var priorityOtherRaw: Int = 0
dynamic var user: User!
var messages = List<Message>()
private let tasks = LinkingObjects(fromType: Task.self, property: "matches")
var task: Task?
dynamic var notification: Notification?
override static func primaryKey() -> String? {
return "id"
}
override static func ignoredProperties() -> [String] {
return ["task"]
}
func getId() -> Int {
return id
}
func getSection() -> Int {
return 0
}
func getURL() -> URL? {
if let image = user.getImageMain() {
return image.getResizedURL()
}
return nil
}
func getPriorityOwn() -> PriorityType {
if priorityOwnRaw == PriorityType.normal.rawValue {
return PriorityType.normal
}
else {
return PriorityType.favorite
}
}
func getPriorityOther() -> PriorityType {
if priorityOtherRaw == PriorityType.normal.rawValue {
return PriorityType.normal
}
else {
return PriorityType.favorite
}
}
func getSectionFieldName() -> String? {
return nil
}
func getTask() -> Task {
return (task != nil ? task : tasks.first)!
}
}
I spent hours trying to figure out why I am unable to keep the matches relationship when updating the task. Every advice will be highly appreciated!
This question was also asked upon Realm's GitHub issue tracker. For posterity, here is the solution.
List properties should always be declared as let properties, as assigning to them does not do anything useful. The correct way to copy all objects from one List to another is model.tasks.append(objectsIn: _user.tasks).