Search code examples
unit-testingrx-swiftrxtest

Testing ViewModel in RxSwift


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


Solution

  • 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 == ""}
    }