Mocking and Validating Results in RxSwift Unit Testing - ios

I have just started learning RxSwift and trying to build a sample application to practice these concepts.
I have written a QuestionViewModel that loads list of questions from QuestionOps class. QuestionOps has a function getQuestions that returns Single<[Question]>.
Problem that I am facing is, how to mock the behavior of QuestionOps class while testing QuestionViewModel.
public class QuestionsListViewModel {
public var questionOps: QuestionOps!
private let disposeBag = DisposeBag()
private let items = BehaviorRelay<[QuestionItemViewModel]>(value: [])
public let loadNextPage = PublishSubject<Void>()
public var listItems: Driver<[QuestionItemViewModel]>
public init() {
listItems = items.asDriver(onErrorJustReturn: [])
loadNextPage
.flatMapFirst { self.questionOps.getQuestions() }
.map { $0.map { QuestionItemViewModel($0) } }
.bind(to: items)
.disposed(by: disposeBag)
}
}
public class QuestionOps {
public func getQuestions() -> Single<[Question]> {
return Single.create { event -> Disposable in
event(.success([]))
return Disposables.create()
}
}
}
I have created this MockQuestionOps for test purpose:
public class MockQuestionOps : QuestionOps {
//MARK: -
//MARK: Responses
public var getQuestionsResponse: Single<[Question]>?
public func getQuestions() -> Single<[Question]> {
self.getQuestionsResponse = Single.create { event -> Disposable in
return Disposables.create()
}
return self.getQuestionsResponse!
}
}
In my test case I am doing the following:
/// My idea here is to test in following maner:
/// - at some point user initates loading
/// - after some time got network response with status true
func testLoadedDataIsDisplayedCorrectly() {
scheduler = TestScheduler(initialClock: 0)
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps()
vm = QuestionsListViewModel()
vm.questionOps = qOps
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
// User initiates load questions
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
// Simulating question ops behaviour of responding
// to get question request
/// HERE: -----------
/// This is where I am stuck
/// How should I tell qOps to send particular response with delay
scheduler.start()
/// HERE: -----------
/// How can I test list is initialy empty
/// and after loading, data is correctly loaded
}

Here is a complete, compilable answer (not including imports.)
You tell qOps to emit by giving it a cold test observable that will emit the correct values.
You test the output by comparing the events gathered by the test observer with the expected results.
There is no notion of "the list is initially empty". The list is always empty. It emits values over time and what you are testing is whether it emitted the correct values.
class rx_sandboxTests: XCTestCase {
func testLoadedDataIsDisplayedCorrectly() {
let scheduler = TestScheduler(initialClock: 0)
let disposebag = DisposeBag()
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps(scheduler: scheduler)
let vm = QuestionsListViewModel(questionOps: qOps)
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
scheduler.start()
XCTAssertEqual(questionsLoadedObserver.events, [.next(12, [QuestionItemViewModel(), QuestionItemViewModel()])])
}
}
protocol QuestionOpsType {
func getQuestions() -> Single<[Question]>
}
struct MockQuestionOps: QuestionOpsType {
func getQuestions() -> Single<[Question]> {
return scheduler.createColdObservable([.next(10, [Question(), Question()]), .completed(10)]).asSingle()
}
let scheduler: TestScheduler
}
class QuestionsListViewModel {
let listItems: Driver<[QuestionItemViewModel]>
private let _loadNextPage = PublishSubject<Void>()
var loadNextPage: AnyObserver<Void> {
return _loadNextPage.asObserver()
}
init(questionOps: QuestionOpsType) {
listItems = _loadNextPage
.flatMapFirst { [questionOps] in
questionOps.getQuestions().asObservable()
}
.map { $0.map { QuestionItemViewModel($0) } }
.asDriver(onErrorJustReturn: [])
}
}
struct Question { }
struct QuestionItemViewModel: Equatable {
init() { }
init(_ question: Question) { }
}

Related

Swift 5: How to get the value of a BehaviorRelay and binding with RxSwift and MVVM

I'm trying to get the amount value from the service in the View Model, and then bind it in the ViewController to an amountLabel.
This is my ViewModel:
class AmountViewModel {
private let accountService: AccountManagerProtocol
let _amount = BehaviorRelay<Int>(value: 0)
let amount: Observable<Int>
private let disposeBag = DisposeBag()
init(accountService: AccountManagerProtocol = AccountManager()) {
self.accountService = accountService
amount = _amount.asObservable()
getAmount()
}
func getAmount(){
accountService.getAccount()
.map{ $0.amount ?? 0 }
.bind(to: _amount)
.disposed(by: disposeBag)
}
}
This is my ViewController,
I did something like this, to obtain the amount of the viewModel, but I feel that it is not the best way, I would like to obtain the value of amount and be able to bind it to amountLabel in a simpler way.
private extension AmountViewController {
private func bindViewModel() {
amountView.titleLabel.text = viewModel.title
//Get Amount
viewModel.amount
.observe(on: MainScheduler.instance)
.withUnretained(self)
.subscribe(onNext: { owner, amount in
if let amountString = amount.currencyToString() {
owner.inputAmountView.amountLabel.text = "BALANCE: \(amountString)"
}
})
.disposed(by: disposeBag)
}
Here is the most obvious simplification:
class AmountViewModel {
let amount: Observable<Int>
init(accountService: AccountManagerProtocol = AccountManager()) {
amount = accountService.getAccount()
.map { $0.amount ?? 0 }
}
}
private extension AmountViewController {
private func bindViewModel() {
viewModel.amount
.compactMap { $0.currencyToString().map { "BALANCE: \($0)"} }
.observe(on: MainScheduler.instance)
.bind(to: inputAmountView.amountLabel.rx.text)
.disposed(by: disposeBag)
}
}
But I think I would move the code in the compactMap closure into the view model...

RxSwift - Determining whether an Observable has been disposed

I'm trying to get Publisher which vends Observables to its clients Consumer, to determine when one of its consumers has disposed of its Observable.
Annoyingly. my code was working fine, until I removed an RxSwift .debug from within the Consumer code.
Is there some alternative way I might get this working?
private class Subscriber {
var ids: [Int]
// This property exists so I can watch whether the observable has
// gone nil (which I though would happen when its been disposed, but it
// seems to happen immediately)
weak var observable: Observable<[Updates]>?
}
class Publisher {
private let relay: BehaviorRelay<[Int: Updates]>
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.asObservable()
let subscriber = Subscriber(ids: ids, observable: observable)
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
let updates: [Updates] = ....
// I need to be able to determine at this point whether the subscriber's
// observable has been disposed of, so I can remove it from the list of
// subscribers. However `subscriber.observable` is always nil here.
// PS: I am happy for this to happen before the `repeatTimer` func fires
subscribers.remove(where: { subscriber in
return subscriber.observable == nil
})
relay.accept(updates)
}
}
class Client {
private var disposeBag: DisposeBag?
private let publisher = Publisher()
func startWatching() {
let disposeBag = DisposeBag()
self.disposeBag = disposeBag
publisher
// with the `.debug` below things work OK, without it the
///`Publisher.Subscriber.observable` immediately becomes nil
//.debug("Consumer")
.updatesStream(for: [1, 2, 3])
.subscribe(onNext: { values in
print(values)
})
.disposed(by: disposeBag)
}
func stopWatching() {
disposeBag = nil
}
}
I think this is a very bad idea, but it solves the requested problem... If I had to put this code in one of my projects, I would be very worried about race conditions...
struct Subscriber {
let ids: [Int]
var subscribeCount: Int = 0
let lock = NSRecursiveLock()
}
class Publisher {
private let relay = BehaviorRelay<[Int: Updates]>(value: [:])
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
var subscriber = Subscriber(ids: ids)
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.do(
onSubscribe: {
subscriber.lock.lock()
subscriber.subscribeCount += 1
subscriber.lock.unlock()
},
onDispose: {
subscriber.lock.lock()
subscriber.subscribeCount -= 1
subscriber.lock.unlock()
})
.asObservable()
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
subscribers.removeAll(where: { subscriber in
subscriber.subscribeCount == 0
})
}
}

How to bind data from viewModel in view with rxSwift and Moya?

I'm trying to create an app to get some news from an API and i'm using Moya, RxSwift and MVVM.
This is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
public enum NewsListError {
case internetError(String)
case serverMessage(String)
}
enum ViewModelState {
case success
case failure
}
protocol NewsListViewModelInput {
func viewDidLoad()
func didLoadNextPage()
}
protocol MoviesListViewModelOutput {
var newsList: PublishSubject<NewsList> { get }
var error: PublishSubject<String> { get }
var loading: PublishSubject<Bool> { get }
var isEmpty: PublishSubject<Bool> { get }
}
protocol NewsListViewModel: NewsListViewModelInput, MoviesListViewModelOutput {}
class DefaultNewsListViewModel: NewsListViewModel{
func viewDidLoad() {
}
func didLoadNextPage() {
}
private(set) var currentPage: Int = 0
private var totalPageCount: Int = 1
var hasMorePages: Bool {
return currentPage < totalPageCount
}
var nextPage: Int {
guard hasMorePages else { return currentPage }
return currentPage + 1
}
private var newsLoadTask: Cancellable? { willSet { newsLoadTask?.cancel() } }
private let disposable = DisposeBag()
// MARK: - OUTPUT
let newsList: PublishSubject<NewsList> = PublishSubject()
let error: PublishSubject<String> = PublishSubject()
let loading: PublishSubject<Bool> = PublishSubject()
let isEmpty: PublishSubject<Bool> = PublishSubject()
func getNewsList() -> Void{
print("sono dentro il viewModel!")
NewsDataService.shared.getNewsList()
.subscribe { event in
switch event {
case .next(let progressResponse):
if progressResponse.response != nil {
do{
let json = try progressResponse.response?.map(NewsList.self)
print(json!)
self.newsList.onNext(json!)
}
catch _ {
print("error try")
}
} else {
print("Progress: \(progressResponse.progress)")
}
case .error( _): break
// handle the error
default:
break
}
}
}
}
This is my ViewController, where xCode give me the following error when i try to bind to tableNews:
Expression type 'Reactive<_>' is ambiguous without more context
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var tableNews: UITableView!
let viewModel = DefaultNewsListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
}
private func setupBindings() {
viewModel.newsList.bind(to: tableNews.rx.items(cellIdentifier: "Cell")) {
(index, repository: NewsList, cell) in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.disposed(by: disposeBag)
}
}
This is the service that get data from API:
import Moya
import RxSwift
struct NewsDataService {
static let shared = NewsDataService()
private let disposable = DisposeBag()
private init() {}
fileprivate let newsListProvider = MoyaProvider<NewsService>()
func getNewsList() -> Observable<ProgressResponse> {
self.newsListProvider.rx.requestWithProgress(.readNewsList)
}
}
I'm new at rxSwift, I followed some documentation but i'd like to know if i'm approaching in the right way. Another point i'd like to know is how correctly bind my tableView to viewModel.
Thanks for the support.
As #FabioFelici mentioned in the comments, UITableView.rx.items(cellIdentifier:) is expecting to be bound to an Observable that contains an array of objects but your NewsListViewModel.newsList is an Observable<NewsList>.
This means you either have to extract the array out of NewsList (assuming it has one) through a map. As in newsList.map { $0.items }.bind(to:...
Also, your MoviesListViewModelOutput should not be full of Subjects, rather it should contain Observables. And I wouldn't bother with the protocols, struts are fine.
Also, your view model is still very imperative, not really in an Rx style. A well constructed Rx view model doesn't contain functions that are repeatedly called. It just has a constructor (or is itself just a single function.) You create it, bind to it and then you are done.

MVVM with RxSwift

I'm trying to understand mvvm + RxSwift but I got some questions.
I'm currently using this approach which I'm not sure if is the right or can be better. How can I do to like grouping the methods, I mean, maybe something like doFirst(loading = true).doNext(getData).doLast(loading = false).catch(apiError) then subscribe to this event? It's possible?
ViewController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel = UsersViewModel(apiService: apiService)
configureBindings()
}
func configureBindings() {
tableView.delegate = nil
tableView.dataSource = nil
viewModel.isLoading.bind(to: loadingView.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.models
.bind(to: tableView.rx.items(cellIdentifier: "userCell", cellType: UserCell.self)) {(_, _, cell) in
print("Binding the cell items")
}.disposed(by: disposeBag)
tableView.rx.modelSelected(User.self).subscribe(onNext: { value in
print(value)
}).disposed(by: disposeBag)
viewModel.error.filterNil().subscribe(onNext: { (err) in
self.tableView.backgroundView = EmptyView(title: "No Users", description: "No users found")
print("Showing empty view...")
print(err)
}).disposed(by: disposeBag)
}
}
Then in my UsersViewModel:
class UsersViewModel {
var models: Observable<[User]> {
return modelsVariable.asObservable()
}
var isLoading: Observable<Bool> {
return isLoadingVariable.asObservable()
}
var error: Observable<ApiError?> {
return errorVariable.asObservable()
}
private var modelsVariable = BehaviorRelay<[User]>(value: [])
private var isLoadingVariable = BehaviorRelay<Bool>(value: false)
private var errorVariable = BehaviorRelay<ApiError?>(value: nil)
// MARK: - Data Manager
var apiService: API
required init(apiService: API) {
self.apiService = apiService
isLoadingVariable.accept(true)
apiService.GET(EndPoints.USER_LIST, type: Several<User>.self)
.subscribe(onNext: { (model) in
self.isLoadingVariable.accept(false)
self.modelsVariable.accept(model.items)
}, onError: { (err) in
self.isLoadingVariable.accept(false)
self.errorVariable.accept(err as? ApiError)
})
}
}
My 'GET' function just returns a Observable<Several<User>>.
Several:
struct Several {
var items: [User]
}
Is there any improvements that I can do?
It's a little hard to understand what you're asking, but if you're concerned about the imperative nature of your init method, and want to wrap your API call into a continuous Observable sequence that can be repeated, you could do something like this:
class UsersViewModel {
//...
var fetchUsersObserver: AnyObserver<Void> {
return fetchUsersSubject.asObserver()
}
//...
private let fetchUsersSubject = PublishSubject<Void>()
private let disposeBag = DisposeBag()
//...
required init(apiService: API) {
self.apiService = apiService
bindFetchUsers()
}
private func bindFetchUsers() {
fetchUsersSubject
.asObservable()
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(true) })
.flatMap(self.fetchUsers)
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(false) })
.bind(to: modelsVariable)
.disposed(by: disposeBag)
}
private func fetchUsers() -> Observable<[User]> {
return apiService
.GET(EndPoints.USER_LIST, type: Several<User>.self)
.map { $0.items }
.catchError { [weak self] error in
self?.errorVariable.accept(error as? ApiError)
return .just([])
}
}
}
Then, you need only bind a control to this AnyObserver, or send it an event manually:
func configureBindings() {
// from a control, such as UIButton
someButton
.rx
.tap
.bind(to: viewModel.fetchUsersObserver)
.disposed(by: disposeBag)
// manually
viewModel.fetchUsersObserver.onNext(())
}
Footnote 1: I typically like to make my view models structs so that I don't have to worry about all the [weak self] statements.
Footnote 2: Notice how the fetchUsers() function catches any errors thrown and does not let the error propagate to the outer Observable sequence. This is important because if this outer Observable emits an error event, it can never emit another next event.

RxSwift - subscribe to a method

Is there a way with RxSwift to subscribe to a method which returns a completion block?
Example, let's have this object:
struct Service {
private var otherService = ...
private var initSucceeded = PublishSubject<Bool>()
var initSucceededObservale: Observable<Bool> {
return initSucceeded.asObservable()
}
func init() {
otherService.init {(success) in
self.initSucceeded.onNext( success)
}
}
}
And in a different place have a way to be notified when the service has been initialised:
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
service.init()
Would be there a simpler solution?
I like to use Variables for this sort of thing. Also, I'd recommend using class here because you're tracking unique states and not just concerning yourself with values.
class Service {
private let bag = DisposeBag()
public var otherService: Service?
private var isInitializedVariable = Variable<Bool>(false)
public var isInitialized: Observable<Bool> {
return isInitializedVariable.asObservable()
}
public init(andRelyOn service: Service? = nil) {
otherService = service
otherService?.isInitialized
.subscribe(onNext: { [unowned self] value in
self.isInitializedVariable.value = value
})
.addDisposableTo(bag)
}
public func initialize() {
isInitializedVariable.value = true
}
}
var otherService = Service()
var dependentService = Service(andRelyOn: otherService)
dependentService.isInitialized
.debug()
.subscribe({
print($0)
})
otherService.initialize() // "Initializes" the first service, causing the second to set it's variable to true.
You could use a lazy property:
lazy let initSucceededObservale: Observable<Bool> = {
return Observable.create { observer in
self.otherService.init {(success) in
observer.on(.next(success))
observer.on(.completed)
}
return Disposables.create()
}
}()
and then you can use:
service.init()
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
Let me know in the comments if you have problems before downvoting, thanks.

Resources