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.
Related
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.
protocol Base {
associatedtype M
var data:M { get set }
func update(data:M)
}
class ViewA : Base {
var data: String = ""
func update(data: String) {}
}
class ViewB : Base {
var data: Int = 2
func update(data: Int) {}
}
var dataArr : [Any] = ["1",2]
var viewArr : [Any] = [ViewA(), ViewB()]
func updatData() {
func update<C, T>(view:C, data:T) where C : Base, C.M == T {
view.update(data: data)
}
for i in 0..<2 {
let view = viewArr[i]
let data = dataArr[I]
// ! there is a errr here, Protocol 'Any' as a type cannot conform to 'Base'
update(view: view, data: data)
}
}
My Views conform to this 'Base' protocol which define what type of data my view use
And I want to implement this updatData function dynamic tell if data can be send to view (base on viwe.m type is same to data's type)
But it is seems to be impossible in Swift?
Array of Any object pass as Base Protocol create conflict.
so add one more class for Base class of ViewA and ViewB
protocol Base {
associatedtype M
var data: M { get set }
func update(data: M)
}
class Base1: Base {
typealias M = Any
var data: Any = ""
func update(data: Any) {
print("Udate \(data)")
}
}
class ViewA: Base1 {
//typealias M = String
//var data: String = ""
override func update(data: Any) {
print("test")
}
}
class ViewB: Base1 {
//typealias M = Int
//var data: Int = 2
override func update(data: Any) {
print(data)
print("Udate")
}
}
var dataArr: [Any] = ["1", 2]
var viewArr: [Any] = [ViewA(), ViewB()]
func updatData() {
func update<C, T>(view: C, data: T) where C: Base, C.M == T {
view.update(data: data)
}
for i in 0..<2 {
let view = viewArr[i]
let data = dataArr[i]
update(view: view as! Base1, data: data)
}
}
You want to create a type erased Base concrete type here: AnyBase:
public final class AnyBase: Base {
fileprivate let _boxed: _Box<M>
public init<Concrete: Base>(_ concrete: Concrete) where Concrete.M == M {
self._boxed = _ConcreteBox(concrete)
}
// Base conformance
public var data: M { _boxed.data }
public func update(data: M) {
_boxed.update(data: data)
}
// Type erasure
fileprivate class _Box<T>: Base {
init() {
guard type(of: self) != _Box.self else { fatalError("Can't create _Box instances, create a sub class instance instead ") }
}
// Base conformance
var base: M {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
func update(data: M) {
fatalError("Must override")
}
}
fileprivate final class _ConcreteBox<Concrete: Base>: _Box<Concrete.M> where M == Concrete.M {
let _concrete: Concrete
init(_ concrete: Concrete) {
self._concrete = concrete
super.init()
}
// Base conformance
override var data: {
get { _concrete.data }
set { _concrete.data = newValue }
}
override func update(data: M) {
_concrete.update(data: data)
}
}
}
Now you can store different concrete instances of Base<M> in an array by adopting the type erased AnyBase<M> instead of using Any in your arrays.
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
}
}
I have a page controller where I added UIViewControllers and display a bunch of form in each viewcontroller. The issue I am facing now is that I need to get the data supplied in each of the forms and save it which is done in the last view controller. I have tried using delegates but the moment the next button is clicked, the previous value stored becomes nil and only the value of the latest VC is displayed. How can I pass data in this textfields. Any help is appritated.
My delegate
protocol NextDelegate: AnyObject {
func next(pageIndex: Int, model: CreatePropertyModel)
func previous(pageIndex: Int, model: CreatePropertyModel)
}
how I created array of VC
lazy var controllers: [UIViewController] = {
let descVC = DescVC()
descVC.delegate = self
let priceVC = PriceVC()
priceVC.delegate = self
let featuresVC = FeaturesVC()
featuresVC.delegate = self
let picturesVC = PicturesVC()
picturesVC.delegate = self
return [descVC, priceVC, featuresVC, picturesVC]
}()
Model Example
class CreatePropertyModel: DictionaryEncodable {
var title: String?
var desc: String?
var property_type_id: Int?
var property_sub_type_id: Int?
var location_id: Int?
var currency: String?
var price: Int?
}
For all your steps, store it in a singleton.
protocol Answer {
var isDone: Bool { get }
}
class Answer1: Answer {
static public let updatedNotification = Notification.Name("Answer1Updated")
var name: String? {
didSet {
NotificationCenter.default.post(name: Answer1.updatedNotification, object: nil)
}
}
var isDone: Bool {
return name != nil
}
}
class Answer2: Answer {
var age: Int?
var isDone: Bool {
return age != nil
}
}
class Form {
static let shared = Form()
var answers: [Answer] = [Answer1(), Answer2()]
var isDone: Bool {
return answers.allSatisfy { $0.isDone == true }
}
private init() {}
func reset() {
answers = [Answer1(), Answer2()]
}
var answer1: Answer1? {
return Form.shared.answers.filter { $0 is Answer1 }.first as? Answer1
}
var answer2: Answer2? {
return Form.shared.answers.filter { $0 is Answer2 }.first as? Answer2
}
}
Then, in your view controller, read / write values like this.
class MyViewControllerForAnswer1: UIViewController {
var answer: Answer1? {
return Form.shared.answer1
}
var name: String? {
get {
return answer?.name
}
set {
answer?.name = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(answerUpdated(notification:)), name: Answer1.updatedNotification, object: nil)
}
#objc func answerUpdated(notification: Notification) {
// Update your content
}
}
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!