Testing ViewModel in RxSwift - unit-testing

I would like to perform a test in one of my ViewModels that contains a BehaviorRelay object called "nearByCity" that it is bind to BehaviorRelay called "isNearBy". That's how my view model looks like.
class SearchViewViewModel: NSObject {
//MARK:- Properties
//MARK: Constants
let disposeBag = DisposeBag()
//MARK: Vars
var nearByCity:BehaviorRelay<String?> = BehaviorRelay(value: nil)
var isNearBy = BehaviorRelay(value: true)
//MARK:- Constructor
init() {
super.init()
setupBinders()
}
}
//MARK:- Private methods
private extension SearchViewViewModel{
func setupBinders(){
nearByCity
.asObservable()
.distinctUntilChanged()
.map({$0 ?? ""})
.map({$0 == ""})
.bind(to: isNearBy)
.disposed(by: disposeBag)
}
}
The test that i want to perform is to actually verify that when the string is accepted, the bool value also changes according to the function setupBinders().
Any Idea?
Thank you

Here's one way to test:
class RxSandboxTests: XCTestCase {
func testBinders() {
let scheduler = TestScheduler(initialClock: 0)
let source = scheduler.createColdObservable([.next(5, "hello"), .completed(10)])
let sink = scheduler.createObserver(Bool.self)
let disposeBag = DisposeBag()
let viewModel = SearchViewViewModel(appLocationManager: StubManager())
source.bind(to: viewModel.nearByCity).disposed(by: disposeBag)
viewModel.isNearBy.bind(to: sink).disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(sink.events, [.next(0, true), .next(5, false)])
}
}
Some other points:
Don't make your subject properties var use let instead because you don't want anybody to be able to replace them with unbound versions.
The fact that you have to use the AppLocationManager in this code that has no need of it implies that the object is doing too much. There is nothing wrong with having multiple view models in a view controller that each handle different parts of the view.
Best would be to avoid using Subjects (Relays) at all in your view model code, if needed, they are better left in the imperative side of the code.
At minimum, break up your setupBinders function so that the parts are independently testable. Your above could have been written as a simple, easily tested, free function:
func isNearBy(city: Observable<String?>) -> Observable<Bool> {
return city
.distinctUntilChanged()
.map {$0 ?? ""}
.map {$0 == ""}
}

Related

How do I track down the dependency that causes a refresh?

I have a fairly complex document type to work with. It is basically a bundle containing a set of independent documents of the same type, with various pieces of metadata about the documents. The data structure that represents the bundle is an array of structs, similar to this (there are several more fields, but these are representative):
struct DocumentData: Equatable, Identifiable, Hashable {
let id = UUID()
var docData: DocumentDataClass
var docName: String
var docFileWrapper: FileWrapper?
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
static func ==(lhs: KeyboardLayoutData, rhs: KeyboardLayoutData) -> Bool {
return lhs.id == rhs.id
}
}
The window for the bundle is a master-detail, with a list on the left and, when one is selected, there is an edit pane for the document on the right. The FileWrapper is used to keep track of which files need to be written for saving, so it gets initialised on reading the relevant file, and reset when an undoable change is made. That is largely the only way that the DocumentData structure gets changed (ignoring explicit things like changing the name).
I've reached a point where a lot of things are working, but I'm stuck on one. There's a view inside the edit pane, several levels deep, and when I double-click it, I want a sheet to appear. It does so, but then disappears by itself.
Searching for ways to work this out, I discovered by using print(Self._printChanges()) at various points that the edit pane was being refreshed after showing the sheet, which meant that the parent disappeared. What I found was that the dependency that changed was the DocumentData instance. But, I then added a print of the DocumentData instance before the _printChanges call, and it is identical. I have also put in didSet for each field of DocumentData to print when they get set, and nothing gets printed, so I'm not sure where the change is happening.
So the question comes down to how I can work out what is actually driving the refresh, since what is claimed to be different is identical in every field.
There are some other weird things happening, such as dragging and dropping text into the view causing the whole top-level document array of DocumentData items to change before the drop gets processed and the data structures get updated, so there are things I am not understanding as clearly as I might like. Any guidance is much appreciated.
ADDED:
The view that triggers the sheet is fairly straightforward, especially compared to its enclosing view, which is where most of the interface code is. This is a slightly simplified version of it:
struct MyView: View, DropDelegate {
#EnvironmentObject var keyboardStatus: KeyboardStatus
#Environment(\.displayFont) var displayFont
#Environment(\.undoManager) var undoManager
var keyCode: Int
#State var modifiers: NSEvent.ModifierFlags = []
#State private var dragHighlight = false
#State private var activeSheet: ActiveSheet?
#State private var editPopoverIsPresented = false
// State variables for double click and drop handling
...
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack {
BackgroundView(...)
Text(...)
}
.onAppear {
modifiers = keyboardStatus.currentModifiers
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(geometry.contentPath)
.onHover { entered in
// updates an inspector view
}
.onTapGesture(count: 2) {
interactionType = .doubleClick
activeSheet = .doubleClick
}
.onTapGesture(count: 1) {
handleItemClick()
}
.sheet(item: $activeSheet, onDismiss: handleSheetReturn) { item in
switch item {
case .doubleClick:
DoubleClickItem(...) ) {
activeSheet = nil
}
case .drop:
DropItem(...) {
activeSheet = nil
}
}
}
.popover(isPresented: $editPopoverIsPresented) {
EditPopup(...)
}
.onDrop(of: KeyCap.dropTypes, delegate: self)
.contextMenu {
ItemContextMenu(...)
}
}
func handleItemClick() {
NotificationCenter.default.post(name: .itemClick, object: nil, userInfo: [...])
}
func handleEvent(event: KeyEvent) {
if event.eventKind == .dropText {
interactionType = .drop
activeSheet = .drop
}
else if event.eventKind == .replaceText {
...
handleItemDoubleClick()
}
}
func handleSheetReturn() {
switch interactionType {
case .doubleClick:
handleItemDoubleClick()
case .drop:
handleItemDrop()
case .none:
break
}
}
func handleItemDoubleClick() {
switch itemAction {
case .state1:
...
case .state2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
func handleItemDrop() {
switch itemDropAction {
case .action1:
...
case .action2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
// Drop delegate
func dropEntered(info: DropInfo) {
dragHighlight = true
}
func dropExited(info: DropInfo) {
dragHighlight = false
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: MyView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
let event = ...
handleEvent(event: event)
}
}
return true
}
return false
}
}
Further edit:
I ended up rewiring the code so that the sheet belongs to the higher level view, which makes everything work without solving the question. I still don't understand why I get a notification that a dependency has changed when it is identical to what it was before, and none of the struct's didSet blocks are called.
Try removing the class from the DocumentData. The use of objects in SwiftUI can cause these kind of bugs since it’s all designed for value types.
Try using ReferenceFileDocument to work with your model object instead of FileDocument which is designed for a model of value types.
Try using sheet(item:onDismiss:content:) for editing. I've seen people have the problem you describe when they try to hack the boolean sheet to work with editing an item.

Unit testing SwiftUI/Combine #Published boolean values

I am trying to acquaint myself with unit testing some view models in SwiftUI. The view model currently has two #Published boolean values that publish changes when an underlying UserDefaults property changes. For my unit tests, I have followed this guide on how to setup UserDefaults for testing so my production values are not modified. I am able to test the default value as such:
func testDefaultValue() {
XCTAssertFalse(viewModel.canDoThing)
}
How would I go about toggling the #Published value then ensuring my view model has received the changes? So for instance, I have a reference to my mock user defaults in my XCTestCase. I attempted to do the following with zero success:
func testValueTogglesToTrue() {
defaults.canDoThing = true
XCTAssertTrue(viewModel.canDoThing)
}
The thought being that updating the underlying user defaults value that is publishing changes to the published value in the view model will notify our view model. The above does not do anything to the view model variable. Do I need to subscribe to the publisher and use sink to accomplish this?
Let's say you store a flag in UserDefaults to know whether the user has completed onboarding:
extension UserDefaults {
#objc dynamic public var completedOnboarding: Bool {
bool(forKey: "completedOnboarding")
}
}
You have a ViewModel which tells your View whether to show onboarding or not and has a method to mark onboarding as completed:
class ViewModel: ObservableObject {
#Published private(set) var showOnboarding: Bool = true
private let userDefaults: UserDefaults
public init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
self.showOnboarding = !userDefaults.completedOnboarding
userDefaults
.publisher(for: \.completedOnboarding)
.map { !$0 }
.receive(on: RunLoop.main)
.assign(to: &$showOnboarding)
}
public func completedOnboarding() {
userDefaults.set(true, forKey: "completedOnboarding")
}
}
To test this class you have a XCTestCase:
class MyTestCase: XCTestCase {
private var userDefaults: UserDefaults!
private var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
try super.setUpWithError()
userDefaults = try XCTUnwrap(UserDefaults(suiteName: #file))
userDefaults.removePersistentDomain(forName: #file)
}
// ...
}
Some of the test cases are synchronous for example you can easily test that showOnboarding depends on UserDefaults completedOnboarding property:
func test_whenCompletedOnboardingFalse_thenShowOnboardingTrue() {
userDefaults.set(false, forKey: "completedOnboarding")
let subject = ViewModel(userDefaults: userDefaults)
XCTAssert(subject.showOnboarding)
}
func test_whenCompletedOnboardingTrue_thenShowOnboardingFalse() {
userDefaults.set(true, forKey: "completedOnboarding")
let subject = ViewModel(userDefaults: userDefaults)
XCTAssertFalse(subject.showOnboarding)
}
Some test are asynchronous, which means you need to use XCTExpectations to wait for the #Published value to change:
func test_whenCompleteOnboardingCalled_thenShowOnboardingFalse() {
let subject = ViewModel(userDefaults: userDefaults)
// first define the expectation that showOnboarding will change to false (1)
let showOnboardingFalse = expectation(
description: "when completedOnboarding called then show onboarding is false")
// subscribe to showOnboarding publisher to know when the value changes (2)
subject
.$showOnboarding
.filter { !$0 }
.sink { _ in
// when false received fulfill the expectation (5)
showOnboardingFalse.fulfill()
}
.store(in: &cancellables)
// trigger the function that changes the value (3)
subject.completedOnboarding()
// tell the tests to wait for your expectation (4)
waitForExpectations(timeout: 0.1)
}

AVSpeechSynthesizerDelegate implementation in SwiftUI

Can anyone share how we can implement AVSpeechSynthesizerDelegate in SwiftUI.
how we can listen to delegate callbacks methods in SwiftUI app.
Thanks
One solution would be to define a class which conforms to ObservableObject. The idea would be to use an #Published property to enable SwiftUI to make updates to your UI. Here's an example of a simple way to keep track of the state of an AVSpeechSynthesizer (I'm unsure of your actual use case):
final class Speaker: NSObject, ObservableObject {
#Published var state: State = .inactive
enum State: String {
case inactive, speaking, paused
}
override init() {
super.init()
synth.delegate = self
}
func speak(words: String) {
synth.speak(.init(string: words))
}
private let synth: AVSpeechSynthesizer = .init()
}
Then, make this class conform to AVSpeechSynthesizerDelegate like so:
extension Speaker: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
self.state = .speaking
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
self.state = .paused
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.state = .inactive
}
// etc...
}
Here, I've simply used the delegate callbacks to update a single #Published property, but you could update however you like here depending on your use case. The main point to bear in mind with ObservableObjects is using the #Published property wrapper for any properties you wish to drive UI updates upon a change in value. Here's an example view:
struct MyView: View {
#ObservedObject var speaker: Speaker
var body: some View {
// 1
Text("State = \(speaker.state.rawValue)")
.onReceive(speaker.$state) { state in
// 2
}
}
}
Note how there's two ways to use #Published properties in SwiftUI Views. 1: Simply read the value. SwiftUI will update your view upon a value change. 2: Access the #Published property's publisher with the $ prefix. Using Views onReceive method, you can execute code whenever the publisher emits a value.

ReactiveSwift Simple Example

I've read the documentation, gone through their wonderful Playground example, searched S.O., and reached the extent of my google-fu, but I cannot for the life of me wrap my head around how to use ReactiveSwift.
Given the following....
class SomeModel {
var mapType: MKMapType = .standard
var selectedAnnotation: MKAnnotation?
var annotations = [MKAnnotation]()
var enableRouteButton = false
// The rest of the implementation...
}
class SomeViewController: UIViewController {
let model: SomeModel
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem?
init(model: SomeModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
// The rest of the implementation...
}
....how can I use ReactiveSwift to initialize SomeViewController with the values from SomeModel, then update SomeViewController whenever the values in SomeModel change?
I've never used reactive anything before, but everything I read leads me to believe this should be possible. It is making me crazy.
I realize there is much more to ReactiveSwift than what I'm trying to achieve in this example, but if someone could please use it to help me get started, I would greatly appreciate it. I'm hoping once I get this part, the rest will just "click".
First you'll want to use MutableProperty instead of plain types in your Model. This way, you can observe changes to them.
class Model {
let mapType = MutableProperty<MKMapType>(.standard)
let selectedAnnotation = MutableProperty<MKAnnotation?>(nil)
let annotations = MutableProperty<[MKAnnotation]>([])
let enableRouteButton = MutableProperty<Bool>(false)
}
In your ViewController, you can then bind those and observe those however necessary:
class SomeViewController: UIViewController {
let viewModel: Model
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem!
init(viewModel: Model) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
routeButton.reactive.isEnabled <~ viewModel.enableRouteButton
viewModel.mapType.producer.startWithValues { [weak self] mapType in
// Process new map type
}
// Rest of bindings
}
// The rest of the implementation...
}
Note that MutableProperty has both, a .signal as well as a .signalProducer.
If you immediately need the current value of a MutableProperty (e.g. for initial setup), use .signalProducer which immediately sends an event with the current value as well as any changes.
If you only need to react to future changes, use .signal which will only send events for future changes.
Reactive Cocoa 5.0 will add UIKit bindings which you can use to directly bind UI elements to your reactive layer like done with routeButton in the example.

Unit Testing Core Data in XCode 7 Beta/Swift 2

In a Nutshell:
I'm trying to write a unit test for a Core Data model in XCode 7/Swift 2. However, a simple test such as testing that the number of rows in the model is equal to what I know it to be fails (I know there to be one row, but the test sees none). My best guess is that I'm getting a different (clean?) version of managedObjectContext, but I'm not sure how to get the same version as the app. Besides the technique described below (following the linked instructions on Andrew Bancroft's site), I tried adding the application delegate to SOTestTests and getting the managedObjectContext as follows...
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
...but I then get an error that XCode [c]ould not cast value of type 'SOTest.AppDelegate' (0x25bb8) to 'SOTestTests.AppDelegate' (0x2c7bb78).
Longform:
Here are the steps to set up the smallest reproducible example I could come up with:
Simple model: One Entity (Person) with one Attribute (name, type String). Person is unique, name is non-optional.
Create a subclass (Editor > Create NSManagedObject Subclass...)
(Comment out #objc(Person) in Person.swift to avoid a build error.)
Add a single row to the model. (Only run the code that gets an entity and saves it once--comment it out on subsequent runs to avoid getting a conflict error due to the unique property.) Here's the ViewController that's doing this:
import UIKit
import CoreData
class ViewController: UIViewController {
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
override func viewDidLoad() {
super.viewDidLoad()
// Put a new person in the model.
let newPerson = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: self.managedObjectContext) as! Person
newPerson.name = "Harper Lee"
// Save it
do { try self.managedObjectContext.save()
} catch _ { NSLog("There was a problem saving to the database.") }
// Sanity check
let fetchRequest = NSFetchRequest(entityName:"Person")
do {
let fetchedResults = try managedObjectContext.executeFetchRequest(fetchRequest) as? [Person]
NSLog("Number of rows (App): \(fetchedResults!.count)") // 1
} catch _ { NSLog("Well...that went badly.") }
}
}
I then add the Person subclass files to SOTestTests (meta, I know) in Build Phases, setup the managedObjectContext according to the example on Andrew Bancroft's (excellent) site (with mods for Swift 2), and write the test checking if there is one row:
import XCTest
import CoreData
class SOTestTests: XCTestCase {
var managedObjectContext: NSManagedObjectContext!
func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext {
let managedObjectModel = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle()])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
do {
try persistentStoreCoordinator.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil)
} catch _ { NSLog("Problem adding the Persistent Store Coordinator") }
let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
return managedObjectContext
}
override func setUp() {
super.setUp()
self.managedObjectContext = self.setUpInMemoryManagedObjectContext()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// Check the number of rows in NUT_DATA
do {
let fetchRequest = NSFetchRequest(entityName:"Person")
let fetchedResults = try managedObjectContext.executeFetchRequest(fetchRequest) as? [Person]
print("Number of rows (Test): \(fetchedResults!.count)") // Prints 0
XCTAssert(1 == fetchedResults!.count, "Incorrect number of rows in `Person` entity.")
} catch _ { NSLog("Error fetching the entity Person.") }
}
}
Create a new scheme by selecting SOTestTests, and addthe SOTest.app executable to the run scheme.
When I run this, the ViewController class prints out that there is a single row, but the test prints out zero rows, and the test fails.
Any ideas what I'm doing wrong?
Just import the actual project to get access to AppDelegate and other classes.
#testable import SOTest
No need to add everything to the test target.