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:".
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 flatMap
s 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With