Search code examples
swiftreactive-programmingxctestrx-swiftrxtest

How can I assert the output of an observable that uses latest from text inputs


I have a property on my view model:

    let isValid: Driver<Bool>
    let credentials: Driver<(String, String)>
......
        credentials = .combineLatest(bindings.username, bindings.password, resultSelector: { (username, password) -> (String, String) in (username, password) })

        isValid = credentials.map { username, password in username.count > 0 && password.count > 7 }

I'd like to assert that the correct state is set on isValid when valid inputs are set.

My test is passing below, however this doesn't feel like the correct way to test this scenario.

Ideally I'd like to start with my strings as "" and then pass in values as if they had been typed so I can assert the default state is set and then changes.

I also find these lines:

   .do(onNext: { state in
            if state {
                exp.fulfill()
            }
        })

a little "hacky".

   func test_is_valid_state_changes_when_inputs_correct_length() {

        let username: Driver<String> = .of("some_user_name")
        let password: Driver<String> = .of("some_user_password")

        let bindings = LoginViewModel.Bindings(username: username, password: password, loginTap: .empty(), doneTap: .empty())
        let sut = LoginViewModel(dependency: "", bindings: bindings)

        let scheduler = TestScheduler(initialClock: 0)
        let observer = scheduler.createObserver(Bool.self)

        let exp = expectation(description: "isValid Event")

        sut.isValid
            .asObservable()
            .do(onNext: { state in
                if state {
                    exp.fulfill()
                }
            })
            .subscribe(observer)
            .disposed(by: disposeBag)

        scheduler.start()

        waitForExpectations(timeout: 0.5) { error in
            XCTAssertNil(error)
            XCTAssertEqual(observer.events.count, 1)
            XCTAssertTrue(observer.events[0].value.element!) // swiftlint:disable:this force_unwrapping
        }
    }

Solution

  • You need to use TestObservables in order to do the testing you want and you don't need an expectation object because this test will complete without any threading issues.

        func test_is_valid_state_changes_when_inputs_correct_length() {
            let scheduler = TestScheduler(initialClock: 0)
    
            let username = scheduler.createHotObservable([.next(0, ""), .next(10, "h")])
            let password = scheduler.createHotObservable([.next(0, ""), .next(30, "p"), .next(40, "passwor"), .next(50, "password")])
    
            let bindings = LoginViewModel.Bindings(
                username: username.asDriver(onErrorRecover: { _ in XCTFail(); return .empty() }),
                password: password.asDriver(onErrorRecover: { _ in XCTFail(); return .empty() }),
                loginTap: .empty(),
                doneTap: .empty()
            )
            let disposeBag = DisposeBag()
            let sut = LoginViewModel(dependency: "", bindings: bindings)
    
            let observer = scheduler.createObserver(Bool.self)
    
            sut.isValid
                .drive(observer)
                .disposed(by: disposeBag)
    
            scheduler.start()
    
            XCTAssertEqual(observer.events, [
                .next(0, false),
                .next(10, false),
                .next(30, false),
                .next(40, false),
                .next(50, true)
            ])
    
        }