使用 RxTest 来建立基于 RxSwift 的自动化测试

最近开始一个 iOS 新项目,我开始完全用 RxSwift 来构建所有逻辑了。本篇文章将讲述如何通过 RxTest 来架设起一个「响应式」的自动化测试环境。

我学习和使用 Rx 的经历

RxSwift(或 Reactive 响应式编程),我了解还算早,但是一直没有全面地采用它。一方面有项目的原因,经典项目因为忙着加 Feature,牵扯的地方比较多,所以只会谨慎局部使用,采用不多,比如奇点,只在一些和复杂网络请求有关的新特性上使用了它。另一方面,我虽然是一个追求新技术的人,但是也不想「有了锤子见什么都是钉子」,逻辑简单的地方确实没必要用上这个。

但在最近新的项目,因为是完全从 0 开始,并且有比较多的交互逻辑,因此我下决心,一开始就要全面使用 RxSwift。

同时,因为使用了 RxSwift 和 MVVM 构架,我一开始就想到了要充分利用「Testable ViewModel」这个特性,因此在实现 App 第一个登录注册逻辑的时候,我就开始建立起测试环境了。

RxTest 简介

在参考了一些资料以后,我使用 RxTest 这个 RxSwift 官方的测试库来实现自动化测试。简单地说,RxTest 可以和 Xcode 的官方测试套件 XCTest 相结合,完美地实现 Reactive Test(响应式测试)。这是怎么做到的呢?我用一个简单的 demo 来解释。

让我们先集成 RxTest 吧!

集成 RxTest

在项目里,我是使用 Carthage 来管理 RxSwift 等第三方库的。在 App Target 里,使用标准的方式来链接 RxSwift、RxCocoa 等框架,并且用「carthage copy-frameworks」把这些 Framework 复制到 App 包。相信这些大家都集成过,我就不多说了。

而 Test Target,除了需要和主 App 一样链接 RxSwift、RxCocoa 以外,还要多链接一个 RxTest,并且也需要 Copy 这个 Framework。配置如图所示,一目了然,相信我不需要补充文字了:

RxTest and project settings

一个简单的基于 RxTest 的测试

以下是实现用户注册与登录过程中的一个测试案例:测试用户输入手机号的过程中,观察 canSendCode 这个属性的变化,当 canSendCode 值为 true,就表示手机号输入正确,可以发验证码啦。

import XCTest
 // 需要 Import RxSwift 和 RxTest
import RxSwift
import RxTest
@testable import App

class RegisterTests: XCTestCase {

    // 这个是和用户注册 ViewController 绑定的 ViewModel
    var registerViewModel = RegisterViewModel()
    var disposeBag = DisposeBag()

    func testInputPhoneNumber() {
        /*
         * TestScheduler 相当于一个测试控制中心
         * 它会模拟响应式环境中,用户在某一时刻进行某个交互(输入)的情况
         * 参数 initialClock 是虚拟时间,建议从 0 开始
         * */
        let scheduler = TestScheduler(initialClock: 0) 
        /*
         * 建立一个可以得到结果的 Observer,这个角色相当于真实世界中的「我」
         * Bool.self 又是什么呢?相当于「我」只观察「是」或「否」两种结果
         * */
        let canSendObserver = scheduler.createObserver(Bool.self)

        /*
         * 从 scheduler 创建热信号,通过 next 这个方法创建。
         * next(100, ("1862222")) 是什么意思?
         * 表示在 100 这个时间点,信号源发送了 "1862222" 这个信息。
         * 换成真实环境,就是用户在 100 这个时间点输入了 "1862222" 这个字符串。
         * 当然,真实环境下除非用户用粘帖的方式,不然是不可能一次输入完的。
         * 为模拟真实情况,分别在 200、300 这个时间点输入更多信息。
         * 其中 300 这个点,还往前贴删了一个字符。
         * */
        let inputPhoneNumberObservable = scheduler.createHotObservable([next(100, ("1862222")),
                                                                        next(200, ("18622222222")),
                                                                        next(300, ("1862222222"))])

        /*
         * 将 registerViewModel 的 canSendCode 订阅至前面创建的 canSendObserver 上
         * 注:canSendCode 是一个 Observable<Bool>
         * */
        self.registerViewModel.canSendCode
            .subscribe(canSendObserver)
            .addDisposableTo(self.disposeBag)

        /*
         * 最后一步,要将前面创建的热信号源,绑定到 registerViewModel 的 phoneNumber 上,
         * 这样信号发出来后,我们的 ViewModel 才会有相应的反应:即 canSendCode 这个 Observable 对应的变化
         * */
        inputPhoneNumberObservable
            .bind(to: self.registerViewModel.phoneNumber)
            .addDisposableTo(self.disposeBag)

        // 一切准备就绪,开始吧!
        scheduler.start()

        /*
         * 这是期望的测试结果
         * 在 0 这个时间点,由于没有输入,canSendCode 是 false
         * 100 这个时间点,输入了 “1862222”,手机号不完整,canSendCode 也是 false
         * 200 这个时间点,手机号完整了,自然是 true
         * 300 这个时间点,啊,手机号输错了,删除一个字符,又变成 false 了
         * */
        let expectedCanSendEvents = [
            next(0, false),
            next(100, false),
            next(200, true),
            next(300, false),
        ]

        // Assert Equal 一下 Observer 真实的 events(结果)和期望的结果,一样就测试通过
        XCTAssertEqual(canSendObserver.events, expectedCanSendEvents)
    }
}

测试写完了,在 Xcode 里,选中测试的 Target,按下 CMD+U,一阵运行之后,如果结果是 Test Succeeded!那么恭喜你,ViewModel 的逻辑完全正确了!

小结

通过这么一个小例子,尽管我没有暴露 ViewModel 和 ViewController 的代码,但相信大家也能猜测到背后的逻辑。

实际上 ViewModel 里面的东西,是非常纯粹地 bind 至了 ViewController 里的 View 而已,比如例子中的 canSendCode,即绑定到了 sendPassCodeButton 的 isEnabled 的属性。只要 canSendCode 的状态对,Button 的状态就会也对。这就是 Testable ViewModel 的意义——ViewModel 测试通过了,UI 和逻辑也基本上没问题了。

从这点上,使用 RxTest 这种「Reactive Test(响应式测试)」,一定程度上同时兼顾到了 UnitTest 和 UITest。

当然,本文只是展示了一个最基本的 demo,也只用了 RxTest 的最常用的几个方法,还有更复杂的交互的测试,就在以后的文章中再介绍吧。

No Comment

Leave a Comment