Ios ViewController中可观察的RxSwift单元测试
我对RxSwift还很陌生。我有一个具有typeahead/autocomplete功能的视图控制器(即,用户在UITextField中键入内容,并且只要他们输入至少2个字符,就会发出网络请求以搜索匹配的建议)。控制器的Ios ViewController中可观察的RxSwift单元测试,ios,swift,unit-testing,rx-swift,Ios,Swift,Unit Testing,Rx Swift,我对RxSwift还很陌生。我有一个具有typeahead/autocomplete功能的视图控制器(即,用户在UITextField中键入内容,并且只要他们输入至少2个字符,就会发出网络请求以搜索匹配的建议)。控制器的viewDidLoad调用以下方法来设置可观察对象: class TypeaheadResultsViewController: UIViewController { var searchTextFieldObservable: Observable<String>
viewDidLoad
调用以下方法来设置可观察对象:
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
(或者,最好是在searchTextField
)上手动/编程触发事件,以触发第二个订阅中标记为“这是我要测试的部分:”的代码 问题在于self.ctrl.searchTextField.rx.text.onNext(“e”)
不会触发searchTextFieldObservable
onNext
订阅
如果像下面这样直接设置文本值,也不会触发订阅self.ctrl.searchTextField.text=“e”
如果您这样设置textField值:self.ctrl.searchTextField.insertText(“e”)
,则将触发订阅(您的测试应该会成功)
我认为这是因为
UITextField.rx.text
观察了uikeyinport
中的方法。我更喜欢让UIViewControllers远离我的单元测试。因此,我建议将此逻辑移到视图模型
作为赏金解释的详细信息,基本上您要做的是模拟textField的text
属性,以便在需要时触发事件。我建议将其全部替换为模拟值。如果将textField.rx.text.bind(viewModel.query)
作为视图控制器的职责,那么您可以关注单元测试的视图模型,并根据需要手动更改查询变量
类视图模型{
let query:Variable=Variable(nil)
let结果:变量=变量([])
设disposeBag=disposeBag()
init(){
查询
.asObservable()
.flatMap{query in
return query.count>=2?搜索(针对:$0):.just([])
}
.bind(结果)
.处置(由:处置人)
}
func搜索(查询:字符串)->可观察{
// ...
}
}
测试用例:
类类型HeadResultsViewControllerTests:xTestCase{
func testManualChange(){
让viewModel=viewModel()
viewModel.results.value=[/*..,..,..*/]
//这会触发订阅,但不会触发搜索
viewModel.query.value=“1”
//断言结果列表为空
xctasertequal(viewModel.results.value,[]))
}
}
如果您还想测试textField和视图模型之间的连接,那么UI测试更适合
请注意,此示例省略了:
query
(即textField.rx.text.asDriver().drive(viewModel.query)
)viewModel.results.asObservable.subscribe(/*…*/)
)这里可能有一些输入错误,没有经过编译器运行。第一步是将逻辑与效果分开。一旦你这样做了,就很容易测试你的逻辑。在这种情况下,要测试的链是:
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)
视图控制器中的代码变为:
self.searchTextField.rx.text
.resetResults(scheduler: MainScheduler.instance)
.subscribe(
onNext: { [weak self] in
self?.results = nil
}
)
.disposed(by: disposeBag)
现在,让我们考虑一下我们需要在这里测试什么。就我而言,我觉得没有必要测试self?.results=nil
或self.searchTextField.rx.text
,因此可以忽略视图控制器进行测试
所以这只是测试操作员的问题。。。最近有一篇很棒的文章发表了:但是,坦率地说,我没有看到任何需要测试的东西。我可以相信
throttle
、map
和filter
都能按设计工作,因为它们是在RxSwift库中测试的,传入的闭包非常基本,我也看不到测试它们的任何意义。如果您查看rx.text
的底层实现,你会发现它依赖于哪个
只需设置文本,它不会触发任何事件,因此不会触发您的可观察对象。您必须明确发送操作:
textField.text=“某物”
textField.sendActions(对于:.valueChanged)//或.allEditingEvents
如果您在一个框架内进行测试,sendActions
将无法工作,因为该框架缺少UIApplication
。你可以这样做
扩展UIControl{
func模拟(事件:UIControl.事件){
allTargets.forEach{中的目标
操作(forTarget:target,forControlEvent:event)?.forEach{
(目标为NSObject)。执行(选择器($0))
}
}
}
}
...
textField.text=“某物”
textField.simulate(事件:.valueChanged)//或.allEditingEvents
如果删除searchTextFieldObservable
中的油门
操作符,测试是否成功?@kiwisip-否-结果相同。似乎事件从未被触发。也就是说,我在onNext
处理程序的第一行上设置了一个断点,它永远不会被命中(第一次加载视图时除外)。您应该使用RxBlock来测试这个异步任务。考试写得不太好。。。您应该了解如何编写更好的测试。如果您要调试它,您会看到内部某个地方的onNext是异步调用的,当Xctasert已经完成时,它仍然可以运行。我已经更新了测试代码,以显示实际的测试代码,而不是我使用self.ctrl.searchTextField.insertText(“e”)尝试的伪代码
但这似乎仍然不会触发事件。如果您现在移除节流阀
,很遗憾,移除节流阀
并不能修复它。我已经用我的回复更新了我的问题
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 }
}
}
self.searchTextField.rx.text
.resetResults(scheduler: MainScheduler.instance)
.subscribe(
onNext: { [weak self] in
self?.results = nil
}
)
.disposed(by: disposeBag)