You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对ViewController中RxSwift实现的搜索联想Observable进行单元测试?

嘿,刚接触RxSwift的单元测试确实容易摸不着头脑,不过针对你这个输入联想的场景,咱们用RxSwift官方提供的RxTest和RxBlocking就能轻松验证核心逻辑,我给你一步步拆解怎么做:

先明确核心测试点

咱们的目标是验证:

  • 当输入字符少于2个时,不会发起网络请求(也就是你的searchTextFieldObservable不会发出查询事件)
  • 当输入字符≥2个时,会正确发起请求,并且传递的查询字符串和输入一致
  • 如果你的实现加了防抖(比如输入停止N毫秒后才发请求),也要验证这个逻辑(可选)

第一步:准备测试环境

首先确保你在测试target里导入了RxTest和RxBlocking,这两个是RxSwift官方的测试工具包。另外,为了避免测试依赖真实网络,我们需要把网络请求的逻辑抽象成协议,用Mock实现来替代真实服务——这是单元测试的核心原则:隔离依赖。

比如先定义一个搜索服务的协议:

protocol SearchServiceType {
    func fetchSuggestions(for query: String) -> Observable<[String]>
}

然后让你的ViewController依赖这个协议,而不是直接创建真实的网络请求实例(比如在初始化时注入这个服务)。

第二步:编写Mock搜索服务

这个Mock的作用是捕获是否被调用,以及调用时的参数,方便我们断言:

class MockSearchService: SearchServiceType {
    // 记录所有被调用的查询字符串
    var calledQueries: [String] = []
    
    func fetchSuggestions(for query: String) -> Observable<[String]> {
        calledQueries.append(query)
        // 返回一个空的Observable,不影响逻辑测试
        return Observable.just([])
    }
}

第三步:编写具体的测试用例

接下来我们用TestScheduler来模拟UITextField的输入事件,精确控制时间和输入内容,然后验证searchTextFieldObservable的行为。

这里是完整的测试类示例:

import XCTest
import RxSwift
import RxTest
import RxBlocking
@testable import YourAppName

class SearchViewControllerTests: XCTestCase {
    var disposeBag: DisposeBag!
    var testScheduler: TestScheduler!
    var mockSearchService: MockSearchService!
    var viewController: YourSearchViewController!

    override func setUp() {
        super.setUp()
        disposeBag = DisposeBag()
        testScheduler = TestScheduler(initialClock: 0)
        mockSearchService = MockSearchService()
        // 初始化ViewController,注入Mock服务
        viewController = YourSearchViewController(searchService: mockSearchService)
        // 触发viewDidLoad(因为loadViewIfNeeded会加载视图,调用viewDidLoad)
        viewController.loadViewIfNeeded()
    }

    override func tearDown() {
        disposeBag = nil
        testScheduler = nil
        mockSearchService = nil
        viewController = nil
        super.tearDown()
    }

    // 测试:输入少于2个字符时,不触发请求
    func test_InputLessThanTwoCharacters_DoesNotTriggerRequest() {
        // 1. 创建测试观察者,用来监听searchTextFieldObservable的输出
        let testObserver = testScheduler.createObserver(String.self)
        
        // 2. 将目标Observable绑定到测试观察者
        viewController.searchTextFieldObservable
            .bind(to: testObserver)
            .disposed(by: disposeBag)
        
        // 3. 模拟输入:时间点10时输入"a"(1个字符)
        testScheduler.createHotObservable([
            .next(10, "a")
        ])
        .bind(to: viewController.searchTextField.rx.text.orEmpty)
        .disposed(by: disposeBag)
        
        // 4. 启动测试调度器
        testScheduler.start()
        
        // 5. 断言:没有收到任何查询事件
        XCTAssertEqual(testObserver.events, [])
        // 同时断言Mock服务没有被调用
        XCTAssertTrue(mockSearchService.calledQueries.isEmpty)
    }

    // 测试:输入≥2个字符时,触发正确的请求
    func test_InputTwoOrMoreCharacters_TriggersRequestWithCorrectQuery() {
        let expectedQuery = "swift"
        let testObserver = testScheduler.createObserver(String.self)
        
        viewController.searchTextFieldObservable
            .bind(to: testObserver)
            .disposed(by: disposeBag)
        
        // 模拟输入:时间点10时输入"swift"(5个字符)
        testScheduler.createHotObservable([
            .next(10, expectedQuery)
        ])
        .bind(to: viewController.searchTextField.rx.text.orEmpty)
        .disposed(by: disposeBag)
        
        testScheduler.start()
        
        // 断言:收到了正确的查询字符串
        XCTAssertEqual(testObserver.events, [.next(10, expectedQuery)])
        // 断言Mock服务被调用了正确的查询
        XCTAssertEqual(mockSearchService.calledQueries, [expectedQuery])
    }

    // 可选:测试防抖逻辑(如果你的实现加了debounce)
    func test_InputWithDebounce_TriggersOnlyAfterIdle() {
        let testObserver = testScheduler.createObserver(String.self)
        // 假设你的防抖时间是50毫秒
        let debounceTime: Int = 50
        
        viewController.searchTextFieldObservable
            .bind(to: testObserver)
            .disposed(by: disposeBag)
        
        // 模拟快速输入:"s" → "sw" → "swi",然后停止输入
        testScheduler.createHotObservable([
            .next(10, "s"),
            .next(20, "sw"),
            .next(30, "swi"),
            .completed(100)
        ])
        .bind(to: viewController.searchTextField.rx.text.orEmpty)
        .disposed(by: disposeBag)
        
        testScheduler.start()
        
        // 断言:只收到最后一次的查询"swi",且时间是最后一次输入+防抖时间
        XCTAssertEqual(testObserver.events, [.next(30 + debounceTime, "swi")])
        XCTAssertEqual(mockSearchService.calledQueries, ["swi"])
    }
}

一些关键注意点

  • TestScheduler的作用:它可以模拟时间流逝,让我们精确控制输入事件的触发时机,避免异步测试的不稳定。
  • rx.text.orEmpty:UITextField的rx.text是可选型,用orEmpty转成非空的String,方便我们处理输入内容。
  • 依赖注入:通过注入Mock服务,我们完全隔离了真实网络的影响,只专注于验证逻辑是否正确。
  • disposeBag的管理:每个测试用例都要创建新的disposeBag,避免不同测试用例之间的干扰。

这样你就能轻松验证searchTextFieldObservable的核心逻辑是否符合预期啦!

内容的提问来源于stack exchange,提问作者Shawn Flahave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:53:26