我'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
调用以下方法来设置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)
}
}
这似乎工作正常,但我正在努力弄清楚如何单元测试 searchTextFieldObservable
的行为 . 为了简单起见,我只想要一个单元测试来验证 results
在更改事件后 searchTextField
少于2个字符时设置为 nil
.
我尝试了几种不同的方法 . 我的测试目前看起来像这样:
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)
}
}
基本上,我想知道如何在 searchTextFieldObservable
(或者,最好是在 searchTextField
)上手动/编程触发事件以触发标记为"This is the part I want to test:"的第二个订阅中的代码 .
3 回答
问题是
self.ctrl.searchTextField.rx.text.onNext("e")
不会触发searchTextFieldObservable
onNext
订阅 .如果您直接设置文本值
self.ctrl.searchTextField.text = "e"
,也不会触发订阅 .如果您设置textField值,则订阅将触发(并且您的测试应该成功):
self.ctrl.searchTextField.insertText("e")
.我认为这是因为
UITextField.rx.text
观察了UIKeyInput
中的方法 .我更喜欢让UIViewControllers远离我的单元测试 . 因此,我建议将此逻辑移至视图模型 .
正如您的赏金说明详细说明,基本上您要做的是模拟textField的
text
属性,以便在您需要时触发事件 . 我建议完全用模拟值替换它 . 如果使textField.rx.text.bind(viewModel.query)
负责视图控制器,则可以关注单元测试的视图模型,并根据需要手动更改查询变量 .测试用例:
如果您还想测试textField和视图模型之间的连接,那么UI测试更适合 .
请注意,此示例省略:
视图模型中网络层的依赖注入 .
视图控制器的textField值绑定到
query
(即textField.rx.text.asDriver().drive(viewModel.query)
) .视图控制器观察结果变量(即
viewModel.results.asObservable.subscribe(/* ... */)
) .这里可能有一些拼写错误,没有通过编译器运行它 .
第一步是将逻辑与效果分开 . 一旦你这样做,就很容易测试你的逻辑 . 在这种情况下,您要测试的链是:
效果只是源和接收器(另一个注意效果的地方是链中的任何
flatMap
. )所以让它们分开:(我把它放在一个扩展名中,因为我知道大多数人讨厌免费功能)
并且视图控制器中的代码变为:
现在,让's think about what we actually need to test here. For my part, I don' t感觉需要测试
self?.results = nil
或self.searchTextField.rx.text
,以便可以忽略View控制器进行测试 .所以它最近出版了一篇很棒的文章:https://www.raywenderlich.com/7408-testing-your-rxswift-code但是,坦率地说,我没有看到任何需要测试的内容 . 我可以相信
throttle
,map
和filter
按照设计工作,因为它们在RxSwift库中进行了测试,并且传入的闭包非常基本,我也没有看到测试它们的任何意义 .