Search code examples
iosswiftunit-testingrx-swiftrx-cocoa

How to test the UI Binding between RxSwift Variable and RxCocoa Observable?


I have a simple ViewModel with one property:

class ViewModel {
    var name = Variable<String>("")
}

And I'm binding it to its UITextField in my ViewController:

class ViewController : UIViewController {

    var viewModel : ViewModel!

    @IBOutlet weak var nameField : UITextField!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Binding
        nameField.rx.text
            .orEmpty
            .bind(to: viewModel.name)
            .disposed(by: disposeBag)
    }
}

Now, I want to Unit Test it.

I'm able to fill the nameField.text property though Rx using .onNext event - nameField.rx.text.onNext("My Name Here").

But the viewModel.name isn't being filled. Why? How can I make this Rx behavior work in my XCTestCase?

Please see below the result of my last test run:

Last Test Run Screenshot


Solution

  • I believe the issue you are having is that the binding is not explicitly scheduled on the main thread. I recommend using a Driver in this case:

    class ViewModel {
      var name = Variable<String>("")
    }
    
    class ViewController: UIViewController {
    
      let textField = UITextField()
      let disposeBag = DisposeBag()
      let vm = ViewModel()
    
      override func viewDidLoad() {
        super.viewDidLoad()
        textField.rx.text
            .orEmpty
            .asDriver()
            .drive(vm.name)
            .disposed(by: disposeBag)
    }
    
    // ..
    

    With a Driver, the binding will always happen on the main thread and it will not error out.

    Also, in your test I would call skip on the binding:

    sut.nameTextField.rx.text.skip(1).onNext(name)
    

    because the Driver will replay the first element when it subscribes.

    Finally, I suggest using RxTest and a TestScheduler instead of GCD.

    Let me know if this helps.