Unit-test RxSwift observable in ViewController - ios

I'm quite new to RxSwift. I have a view controller that has a typeahead/autocomplete feature (i.e., user types in a UITextField and as soon as they enter at least 2 characters a network request is made to search for matching suggestions). The controller's viewDidLoad calls the following method to set up an Observable:
class TypeaheadResultsViewController: UIViewController {
var searchTextFieldObservable: Observable<String>!
#IBOutlet weak var searchTextField: UITextField!
private let disposeBag = DisposeBag()
var results: [TypeaheadResult]?
override func viewDidLoad() {
super.viewDidLoad()
//... unrelated setup stuff ...
setupSearchTextObserver()
}
func setupSearchTextObserver() {
searchTextFieldObservable =
self.searchTextField
.rx
.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map { $0 ?? "" }
searchTextFieldObservable
.filter { $0.count >= 2 }
.flatMapLatest { searchTerm in self.search(for: searchTerm) }
.subscribe(
onNext: { [weak self] searchResults in
self?.resetResults(results: searchResults)
},
onError: { [weak self] error in
print(error)
self?.activityIndicator.stopAnimating()
}
)
.disposed(by: disposeBag)
// This is the part I want to test:
searchTextFieldObservable
.filter { $0.count < 2 }
.subscribe(
onNext: { [weak self] _ in
self?.results = nil
}
)
.disposed(by: disposeBag)
}
}
This seems to work fine, but I'm struggling to figure out how to unit test the behavior of searchTextFieldObservable.
To keep it simple, I just want a unit test to verify that results is set to nil when searchTextField has fewer than 2 characters after a change event.
I have tried several different approaches. My test currently looks like this:
class TypeaheadResultsViewControllerTests: XCTestCase {
var ctrl: TypeaheadResultsViewController!
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
ctrl = storyboard.instantiateViewController(withIdentifier: "TypeaheadResultsViewController") as! TypeaheadResultsViewController
}
override func tearDown() {
ctrl = nil
super.tearDown()
}
/// Verify that the searchTextObserver sets the results array
/// to nil when there are less than two characters in the searchTextView
func testManualChange() {
// Given: The view is loaded (this triggers viewDidLoad)
XCTAssertNotNil(ctrl.view)
XCTAssertNotNil(ctrl.searchTextField)
XCTAssertNotNil(ctrl.searchTextFieldObservable)
// And: results is not empty
ctrl.results = [ TypeaheadResult(value: "Something") ]
let tfObservable = ctrl.searchTextField.rx.text.subscribeOn(MainScheduler.instance)
//ctrl.searchTextField.rx.text.onNext("e")
ctrl.searchTextField.insertText("e")
//ctrl.searchTextField.text = "e"
do {
guard let result =
try tfObservable.toBlocking(timeout: 5.0).first() else {
return }
XCTAssertEqual(result, "e") // passes
XCTAssertNil(ctrl.results) // fails
} catch {
print(error)
}
}
Basically, I'm wondering how to manually/programmatically fire an event on searchTextFieldObservable (or, preferably, on the searchTextField) to trigger the code in the 2nd subscription marked "This is the part I want to test:".

The first step is to separate the logic from the effects. Once you do that, it will be easy to test your logic. In this case, the chain you want to test is:
self.searchTextField.rx.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map { $0 ?? "" }
.filter { $0.count < 2 }
.subscribe(
onNext: { [weak self] _ in
self?.results = nil
}
)
.disposed(by: disposeBag)
The effects are only the source and the sink (another place to look out for effects is in any flatMaps in the chain.) So lets separate them out:
(I put this in an extension because I know how much most people hate free functions)
extension ObservableConvertibleType where E == String? {
func resetResults(scheduler: SchedulerType) -> Observable<Void> {
return asObservable()
.throttle(0.5, scheduler: scheduler)
.map { $0 ?? "" }
.filter { $0.count < 2 }
.map { _ in }
}
}
And the code in the view controller becomes:
self.searchTextField.rx.text
.resetResults(scheduler: MainScheduler.instance)
.subscribe(
onNext: { [weak self] in
self?.results = nil
}
)
.disposed(by: disposeBag)
Now, let's think about what we actually need to test here. For my part, I don't feel the need to test self?.results = nil or self.searchTextField.rx.text so the View controller can be ignored for testing.
So it's just a matter of testing the operator... There's a great article that recently came out: https://www.raywenderlich.com/7408-testing-your-rxswift-code However, frankly I don't see anything that needs testing here. I can trust that throttle, map and filter work as designed because they were tested in the RxSwift library and the closures passed in are so basic that I don't see any point in testing them either.

The problem is that self.ctrl.searchTextField.rx.text.onNext("e") won't trigger searchTextFieldObservable onNext subscription.
The subscription is also not triggered if you set the text value directly like this self.ctrl.searchTextField.text = "e".
The subscription will trigger (and your test should succeed) if you set the textField value like this: self.ctrl.searchTextField.insertText("e").
I think the reason for this is that UITextField.rx.text observes methods from UIKeyInput.

I prefer to keep UIViewControllers far away from my unit tests. Therefore, I suggest moving this logic to a view model.
As your bounty explanation details, basically what you are trying to do is mock the textField's text property, so that it fires events when you want it to. I would suggest replacing it with a mock value altogether. If you make textField.rx.text.bind(viewModel.query) the responsibility of the view controller, then you can focus on the view model for the unit test and manually alter the query variable as needed.
class ViewModel {
let query: Variable<String?> = Variable(nil)
let results: Variable<[TypeaheadResult]> = Variable([])
let disposeBag = DisposeBag()
init() {
query
.asObservable()
.flatMap { query in
return query.count >= 2 ? search(for: $0) : .just([])
}
.bind(results)
.disposed(by: disposeBag)
}
func search(query: String) -> Observable<[TypeaheadResult]> {
// ...
}
}
The test case:
class TypeaheadResultsViewControllerTests: XCTestCase {
func testManualChange() {
let viewModel = ViewModel()
viewModel.results.value = [/* .., .., .. */]
// this triggers the subscription, but does not trigger the search
viewModel.query.value = "1"
// assert the results list is empty
XCTAssertEqual(viewModel.results.value, [])
}
}
If you also want to test the connection between the textField and the view model, UI tests are a much better fit.
Note that this example omits:
Dependency injection of the network layer in the view model.
The binding of the view controller's textField value to query (i.e., textField.rx.text.asDriver().drive(viewModel.query)).
The observing of the results variable by the view controller (i.e., viewModel.results.asObservable.subscribe(/* ... */)).
There might be some typos in here, did not run it past the compiler.

If you look at the underlying implementation for rx.text, you'll see it relies on controlPropertyWithDefaultEvents which fires the following UIControl events: .allEditingEvents and .valueChanged.
Simply setting the text, it won't fire any events, so your observable is not triggered. You have to send an action explicitly:
textField.text = "Something"
textField.sendActions(for: .valueChanged) // or .allEditingEvents
If you are testing within a framework, sendActions won't work because the framework is missing the UIApplication. You can do this instead
extension UIControl {
func simulate(event: UIControl.Event) {
allTargets.forEach { target in
actions(forTarget: target, forControlEvent: event)?.forEach {
(target as NSObject).perform(Selector($0))
}
}
}
}
...
textField.text = "Something"
textField.simulate(event: .valueChanged) // or .allEditingEvents

Related

Swift iOS ReactiveKit: calling the observer causes to trigger action multiple times?

I have Singleton class to which i have used to observe a property and trigger next action.
Singleton Class:
public class BridgeDispatcher: NSObject {
open var shouldRespondToBridgeEvent = SafePublishSubject<[String: Any]>()
open var shouldPop = SafePublishSubject<Void>()
open var shouldUpdate = SafePublishSubject<Void>()
public let disposeBag = DisposeBag()
open static let sharedInstance: BridgeDispatcher = BridgeDispatcher()
override init() {
super.init()
shouldRespondToBridgeEvent.observeNext { event in
if let type = event["type"] as? String {
switch type {
case "ShouldUpdate":
self.onShiftBlockDidUpdateHeight.next()
case "shouldPop":
self.onPopCurrentViewController.next(())
default:
print("Event not supported")
}
}
}.dispose(in: self.disposeBag)
}
}
Above method will trigger by calling:
BridgeDispatcher.sharedInstance.shouldRespondToBridgeEvent.next(body)
Register for onPopCurrentViewController:
BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext { doSomething() }.dispose(in: BridgeDispatcher.sharedInstance.disposeBag)
On my application, BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext{} method will be called multiple times due to the business logic, due to this doSomething() method will trigger multiple times when calling BridgeDispatcher.sharedInstance.shouldRespondToBridgeEvent.next(body).
Is this issue with my singleton design pattern or observeNext calling multiple times. (BridgeDispatcher.sharedInstance.onPopCurrentViewController.observeNext{} )
Need help.
I have used .updateSignal on ObservableComponent.
valueToUpdate.updateSignal.compactMap { (arg0) -> String? in
let (value, _, validationFailure) = arg0
return validationFailure == nil ? value?.value : nil
}
.removeDuplicates()
.debounce(for: 1.0)
.observeNext { [unowned self] _ in
self.doYourWork()
}
.dispose(in: self.bag)
It attempts to deal with the multiple calls in two ways: first by discarding any duplicate events, so if the duration hasn’t changed, then no call is made. Second, by debouncing the signal so if the user makes a bunch of changes we only call the method when they’re done making changes.

How can I use Rx Swift PublishRelay with no type just for onCompleted() event?

I have this view model in my code:
import RxSwift
protocol ViewModelInput {
func buttonTouched()
}
protocol ViewModelOutput {
var done : PublishRelay<Bool> { get set }
}
protocol ViewModelType {
var inputs: ViewModelInput { get }
var outputs: ViewModelOutput { get }
}
public final class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
var inputs: ViewModelInput { return self }
var outputs: ViewModelOutput { return self }
internal var done = PublishRelay<Bool>.init()
init() {}
func buttonTouched() {
self.outputs.done.accept(true)
}
}
And I'm using it's "output" like this:
// Somewhere else in my app
viewModel.outputs.done
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
// whatever
}).disposed(by: disposeBag)
To be honest I don't need that Boolean value with PublishRelay. I don't even need onNext() event. All I need is to notify my coordinator (part of app that uses this view model) about onCompleted(). However there is still some <Bool> generic type added to my output. I don't need any of that. Is there any cleaner way to achieve that?
I though about traits like Completable but as far as I understand I need to emit completed-event inside create() method or use Completable.empty(). Or maybe I don't understand traits that good, I don't know.
Any ideas?
I haven't done any RxSwift in a while, but have you tried making the type PublishRelay<Void>? Once you do that you can just pass () to outputs.done.accept(()) in your buttonTouched() method and not have to worry about passing arbitrary information that isn't needed
I think #Steven0351 is right with the < Void> approach. Just 2 little things:
It should also work by terminating the subject instead of emitting a Void value. It looks cleaner in the subscription as well.
I guess you are subscribing your outputs.done subject in the UI. In that case you might want to use Drivers. That way there's no need to specify observation on main scheduler (among other Drivers advantages).
ViewModel
internal var done = PublishRelay<Void>.init()
func buttonTouched() {
self.outputs.done.onCompleted()
}
ViewController
viewModel.outputs.done
.asDriver()
.drive(onCompleted: { [weak self] in
// whatever
}).disposed(by: disposeBag)

Prevent disposal of PublishSubject (RxSwift)

I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )

In the RxSwift-MVVM architecture what is the best way to trigger UI elements like pop-ups and indicators?

The question is - where it’s best to:
Call error handling popups
Show/Hide loading indicator
My app looks like this:
ViewController that subscribes to trigger of UI update when the model changes:
var viewModel: ViewModel = ViewModel()
...
viewModel.source.asObservable().subscribe(onNext: { (_ ) in
self.tableView.reloadData()
})
.disposed(by: bag)
ViewModel
var source = Variable<[Student]>([])
And when initialized it fetches the source output
api.fetchSourceOutput(id: id)
.do(onError: { (error) in
//show error here???
})
.catchErrorJustReturn([])
.bind(to: source)
.disposed(by: bag)
I can't just pass reference of ViewController into ViewModel, that would break the idea of it's independence from UI. Then how am I supposed to call error popup in view controller's view? Getting top view controller is not a good option either, because I might need specific view to show my popup in.
The loading indicator can be shown when onNext called inside viewModel and hidden onCompleted. But I again don't have reference to my view controller where my loading indicator reference resides.
Ideas?
Call error handling popups
Lets say you have some signal which starts api fetch
let someSignalWithIdToStartApiFetch = Observable.just(1)
Also, lets imagine that when you present some "retry request" popup on error and when user clicks on "retry" button you bind it to some observer. Then convert an observer to Observable. So you have some 2nd signal:
let someSignalWhenUserAsksToRetryRequestAfterError = Observable.just(())
When you need to retry a request you take the last id from someSignalWithIdToStartApiFetch this way:
let someSignalWithIdToRetryApiFetch = someSignalWhenUserAsksToRetryRequestAfterError
.withLatestFrom(someSignalWithIdToStartApiFetch)
.share(replay: 1, scope: .whileConnected)
Then you combine both signals and make a request:
let apiFetch = Observable
.of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
.merge()
.flatMap({ id -> Observable<Response> in
return api
.fetchSourceOutput(id: id)
.map({ Response.success($0) })
.catchError({ Observable.just(Response.error($0)) })
})
.share(replay: 1, scope: .whileConnected)
As you can see, the error is caught and converted to some result. For example:
enum Response {
case error(Error)
case success([Student])
var error: Error? {
switch self {
case .error(let error): return error
default: return nil
}
}
var students: [Student]? {
switch self {
case .success(let students): return students
default: return nil
}
}
}
Then you work with successful result as usual:
apiFetch
.map({ $0.students })
.filterNil()
.bind(to: source)
.disposed(by: bag)
But the error case should be bind to some observer which triggers popup to be shown:
apiFetch
.map({ $0.error })
.filterNil()
.bind(to: observerWhichShowsPopUpWithRetryButton)
.disposed(by: bag)
So, when the pop up is shown and user clicks on "retry" - someSignalWhenUserAsksToRetryRequestAfterError will trigger and retry the request
Show/Hide loading indicator
I use something like this. It is a special structure which catches the activity of an observable. How you can use it?
let indicator = ActivityIndicator()
And some code from 1st part of the question.
let apiFetch = Observable
.of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
.merge()
.flatMap({ id -> Observable<[Student]> in
return indicator
.trackActivity(api.fetchSourceOutput(id: id))
})
.map({ Response.success($0) })
.catchError({ Observable.just(Response.error($0)) })
.share(replay: 1, scope: .whileConnected)
So, the activity of api fetch is tracked. Now you should show/hide your activity view.
let observableActivity = indicator.asObservable() // Observable<Bool>
let observableShowLoading = observableActivity.filter({ $0 == true })
let observableHideLoading = observableActivity.filter({ $0 == false })
Bind observableShowLoading and observableHideLoading to hide/show functions. Even if you have multiple request which might be executed simultaneously - bind them all to a single ActivityIndicator.
Hope it helps. Happy coding (^
I would make this changes in your viewModel:
// MARK: - Properties
let identifier = Variable(0)
lazy var source: Observable<[Student]> = identifier.asObservable()
.skip(1)
.flatMapLatest { id in
return api.fetchSourceOutput(id: id)
}
.observeOn(MainScheduler.instance)
.share(replay: 1)
...
// MARK: - Initialization
init(id: Int) {
identifier.value = id
...
}
Then, in your ViewController:
viewModel.source
.subscribe(onNext: { _ in
self.tableView.reloadData()
}, onError: { error in
// Manage errors
})
.disposed(by: bag)

RxSwift: using rx_refreshing for uirefreshcontrol

I am using the UIRefreshControl + Variable binding to reload data.
It is working, however, the following feels wrong to me:
1) I know there is a rx_refreshing variable in the RXCocoa extension, but I am unable to get it to work in this context.
2) I am binding answers (which is a Variable of array) twice. Once when I load the view controller and again when the UIRefreshControl is refreshing.
3) The parts where I check for whether the UIRefreshControl is refreshing or not looks really awkward. It feels like it defeats the purpose of using reactive?
...
let answers: Variable<[Answer]> = Variable([])
override func viewDidLoad() {
loadAnswers()
.shareReplay(1)
.bindTo(answers)
.addDisposableTo(self.disposeBag)
setupRx()
}
func loadAnswers() -> Observable<[Answer]> {
return Network.rxArrayRequest(Spark.Answers)
}
func setupRx() {
rc.rx_controlEvent(.ValueChanged)
.map { _ in !self.rc.refreshing }
.filter { $0 == false }
.flatMapLatest { [unowned self] _ in
return self.loadAnswers()
}
.bindTo(answers)
.addDisposableTo(self.disposeBag)
rc.rx_controlEvent(.ValueChanged)
.map { _ in self.rc.refreshing }
.filter { $0 == true }
.subscribeNext { [unowned self] _ in
self.rc.endRefreshing()
}
.addDisposableTo(self.disposeBag)
}
...
So first of all, It's not actually working. It just seems to be working. In your code, you're actually not waiting for the network request to finish before you call rc.endRefreshing(). Instead, you're just making the network call and then immediately calling endRefreshing().
// `rc.rx_controlEvent(.ValueChanged)` only gets called once,
// when the user pulls down.
rc.rx_controlEvent(.ValueChanged) // user pulled down to refresh
.map { _ in !self.rc.refreshing } // !true -> false
.filter { $0 == false } // false == false
.flatMapLatest { [unowned self] _ in
return self.loadAnswers() // request answers
}
.bindTo(answers)
.addDisposableTo(self.disposeBag)
rc.rx_controlEvent(.ValueChanged) // user pulled down to refresh
.map { _ in self.rc.refreshing } // true -> true
.filter { $0 == true } // true == true
.subscribeNext { [unowned self] _ in
self.rc.endRefreshing() // end refreshing
}
.addDisposableTo(self.disposeBag)
To address concern 1, you're right, you can use rx_refreshing to turn off refreshing instead of endRefreshing().
To address concern 2, I don't think the Variable is necessary or useful, at least in this example. You could still use it though. Also, it's not necessary to loadAnswers() in two places.
To address concern 3, yea, you could be simplifying this a lot and using Rx a bit more.
Here's code that would actually work, use rx_refreshing, and simplify things a lot:
let initial = Observable<Void>.just(())
let refresh = rc.rx_controlEvent(.ValueChanged).map { _ in () }
let answers = Observable.of(initial, refresh)
.merge()
.flatMapLatest{ _ in self.loadAnswers() }
.shareReplayLatestWhileConnected()
answers
.map { _ in false }
.bindTo(rc.rx_refreshing)
.addDisposableTo(disposeBag)
// also use `answers` to bind to your data source, etc.

Resources