Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit-test RxSwift observable in ViewController

I'm quite new to RxSwift. I have a view controller that has a typeahead/autocomplete feature (i.e., user types in a UITextField and as soon as they enter at least 2 characters a network request is made to search for matching suggestions). The controller's viewDidLoad calls the following method to set up an Observable:

class TypeaheadResultsViewController: UIViewController {

var searchTextFieldObservable: Observable<String>!
@IBOutlet weak var searchTextField: UITextField!
private let disposeBag = DisposeBag()
var results: [TypeaheadResult]?

override func viewDidLoad() {
    super.viewDidLoad()
    //... unrelated setup stuff ...
    setupSearchTextObserver()
}

func setupSearchTextObserver() {
            searchTextFieldObservable =
                self.searchTextField
                    .rx
                    .text
                    .throttle(0.5, scheduler: MainScheduler.instance)
                    .map { $0 ?? "" }

            searchTextFieldObservable
                .filter { $0.count >= 2 }
                .flatMapLatest { searchTerm in self.search(for: searchTerm) }
                .subscribe(
                    onNext: { [weak self] searchResults in
                        self?.resetResults(results: searchResults)
                    },
                    onError: { [weak self] error in
                        print(error)
                        self?.activityIndicator.stopAnimating()
                    }
                )
                .disposed(by: disposeBag)

            // This is the part I want to test:        
            searchTextFieldObservable
                .filter { $0.count < 2 }
                .subscribe(
                    onNext: { [weak self] _ in
                        self?.results = nil
                    }
                )
                .disposed(by: disposeBag)
    }
}

This seems to work fine, but I'm struggling to figure out how to unit test the behavior of searchTextFieldObservable. To keep it simple, I just want a unit test to verify that results is set to nil when searchTextField has fewer than 2 characters after a change event.
I have tried several different approaches. My test currently looks like this:

    class TypeaheadResultsViewControllerTests: XCTestCase {
        var ctrl: TypeaheadResultsViewController!

        override func setUp() {
            super.setUp()
            let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
            ctrl = storyboard.instantiateViewController(withIdentifier: "TypeaheadResultsViewController") as! TypeaheadResultsViewController
        }

        override func tearDown() {
            ctrl = nil
            super.tearDown()
        }

        /// Verify that the searchTextObserver sets the results array
        /// to nil when there are less than two characters in the searchTextView
        func testManualChange() {
          // Given: The view is loaded (this triggers viewDidLoad)
          XCTAssertNotNil(ctrl.view)
          XCTAssertNotNil(ctrl.searchTextField)
          XCTAssertNotNil(ctrl.searchTextFieldObservable)

          // And: results is not empty
          ctrl.results = [ TypeaheadResult(value: "Something") ]

          let tfObservable = ctrl.searchTextField.rx.text.subscribeOn(MainScheduler.instance)
          //ctrl.searchTextField.rx.text.onNext("e")
          ctrl.searchTextField.insertText("e")
          //ctrl.searchTextField.text = "e"
          do {
              guard let result =
                try tfObservable.toBlocking(timeout: 5.0).first() else { 
return }
            XCTAssertEqual(result, "e")  // passes
            XCTAssertNil(ctrl.results)  // fails
        } catch {
            print(error)
        }
    }

Basically, I'm wondering how to manually/programmatically fire an event on searchTextFieldObservable (or, preferably, on the searchTextField) to trigger the code in the 2nd subscription marked "This is the part I want to test:".

like image 730
Shawn Flahave Avatar asked Nov 08 '22 10:11

Shawn Flahave


1 Answers

The first step is to separate the logic from the effects. Once you do that, it will be easy to test your logic. In this case, the chain you want to test is:

self.searchTextField.rx.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map { $0 ?? "" }
.filter { $0.count < 2 }
.subscribe(
    onNext: { [weak self] _ in
        self?.results = nil
    }
)
.disposed(by: disposeBag)

The effects are only the source and the sink (another place to look out for effects is in any flatMaps in the chain.) So lets separate them out:

(I put this in an extension because I know how much most people hate free functions)

extension ObservableConvertibleType where E == String? {
    func resetResults(scheduler: SchedulerType) -> Observable<Void> {
        return asObservable()
            .throttle(0.5, scheduler: scheduler)
            .map { $0 ?? "" }
            .filter { $0.count < 2 }
            .map { _ in }
    }
}

And the code in the view controller becomes:

self.searchTextField.rx.text
    .resetResults(scheduler: MainScheduler.instance)
    .subscribe(
        onNext: { [weak self] in
            self?.results = nil
        }
    )
    .disposed(by: disposeBag)

Now, let's think about what we actually need to test here. For my part, I don't feel the need to test self?.results = nil or self.searchTextField.rx.text so the View controller can be ignored for testing.

So it's just a matter of testing the operator... There's a great article that recently came out: https://www.raywenderlich.com/7408-testing-your-rxswift-code However, frankly I don't see anything that needs testing here. I can trust that throttle, map and filter work as designed because they were tested in the RxSwift library and the closures passed in are so basic that I don't see any point in testing them either.

like image 137
Daniel T. Avatar answered Nov 15 '22 11:11

Daniel T.