1

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

Cristik
  • 30,989
  • 25
  • 91
  • 127
Rici
  • 1,014
  • 2
  • 12
  • 21
  • 1
    UI components almost always pose problems when trying to be unit tested, which is why they are not suitable for this task. Test your business logic instead, and let QA do the UI checks. – Cristik Feb 15 '18 at 19:57
  • Good call @Cristik, I'll take your advice. I'm still curious why Rx isn't being fired though – Rici Feb 15 '18 at 20:25
  • 1
    I recommend using the RxTest library and `TestScheduler` instead of GCD. Then you can schedule an action at a specific team and you have much more control. – damianesteban Feb 15 '18 at 22:11
  • 1
    Also `Variable` is deprecated. You have `BehaviorRelay` and `PublishRelay` available as replacements. – Valérian Feb 16 '18 at 08:43

2 Answers2

2

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.

damianesteban
  • 1,603
  • 1
  • 19
  • 36
1

rx.text relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Thus, explicitly send a onNext events will not send action for these event. You should send action manually.

sut.nameField.rx.text.onNext(name)
sut.nameField.sendActions(for: .valueChanged)
JT501
  • 1,407
  • 15
  • 12