Warning: file_get_contents(/data/phpspider/zhask/data//catemap/8/swift/16.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Ios ViewController中可观察的RxSwift单元测试_Ios_Swift_Unit Testing_Rx Swift - Fatal编程技术网

Ios ViewController中可观察的RxSwift单元测试

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>

我对RxSwift还很陌生。我有一个具有typeahead/autocomplete功能的视图控制器(即,用户在UITextField中键入内容,并且只要他们输入至少2个字符,就会发出网络请求以搜索匹配的建议)。控制器的
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测试更适合

请注意,此示例省略了:

  • 视图模型中网络层的依赖项注入
  • 视图控制器的textField值绑定到
    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)