RxSwift 的单元测试

本文最后更新于 2021年4月4日 晚上

本文主要介绍如何对 Rxwift 代码进行单元测试。

RxSwift 单元测试基础

为什么不写单元测试?

  1. 写的代码没有 bug.
  2. 有专业测试人员, 为什么还要写单元测试?
  3. 写单元测试不好玩.
  4. 写单元测试会拖延项目前期推进速度.

为什么写单元测试?

  1. 为了减少 bug.
  2. 写单元测试和有没有专门测试人员没有太大关系, 因为两者的 scope 不同.
  3. 为了增加编程乐趣.
  4. 有富余时间可以写单元测试(可能是少部分核心功能的单元测试).

为什么看这篇文章? 因为想写 RxSwift 客户代码的单元测试.

既然本文标题是 “RxSwift 单元测试基础”, 那肯定就要拿单元测试说点事儿. RxSwift 提供有两个互补的单元测试工具: RxTest 和 RxBlocking, 下面主要围绕这两个工具进行讲解.

针对流测试时候的难点

相比于测试传统的命令式代码, 响应式代码测试时有如下两个难点:

  1. 测试的对象是流中的事件.
  2. 事件是随时间改变而添加到流上的, 需要一种机制来”记录”这些事件的发生, 从而进行测试.

测什么?

根据 APP 的需求, 并结合单元测试的特点, 需要测试:

  • View 的控制代码部分(即除去界面的实现)
  • ViewModel
  • Model

故一般流程是:

  1. 明确需求
  2. 列出待测单元列表
  3. 写测试

主要工具

RxTest

提供对流生成过程的精确控制(TestScheduler 提供的能力). 因为在 RxSwift 中, 通过 Scheduler 来抽象和描述任务的执行方式, 以及调度任务执行结果(即发射的事件).

通过 TestScheduler, 可以创建模拟的 ObservableObserver, 且记录它们上面事件发生时间, 详见官方文档.

用法是准备 TestScheduler 和一个 Disposable 持有每个测试中的订阅, 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var scheduler: TestScheduler!
var disposeBag: DisposeBag!

override func setUpWithError() throws {
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
}

/// 下面演示使用 TestScheduler 来创建一个模拟观察者, 然后创建一个模拟序列
///
/// 通过输入序列驱动观察者, 从而测试 ViewModel 的功能.
func testTappedPlayPauseChangesIsPlaying() {
// 1
let isPlaying = scheduler.createObserver(Bool.self)

// 2
viewModel.isPlaying
.drive(isPlaying)
.disposed(by: disposeBag)

// 3
scheduler.createColdObservable([.next(10, ()),
.next(20, ()),
.next(30, ())])
.bind(to: viewModel.tappedPlayPause)
.disposed(by: disposeBag)

// 4
scheduler.start()

// 5
XCTAssertEqual(isPlaying.events, [
.next(0, false),
.next(10, true),
.next(20, false),
.next(30, true)
])
}

需要注意的是其中的时间是虚拟时间, 在 TestScheduler 的构造方法中还可以指定 resolution 参数, 用以表示虚拟时间的精度(默认是 1).

RxBlocking

允许用户将当前线程阻塞(使用 toBlocking())以获取一段时间内的所有事件, 这样在最后可以进行同步测试. 它适用于有限序列, 即会出现终止事件的序列.

如下三个是最常用的操作符:

  • toArray
  • first
  • last

RxBlocking 的使用非常简单, 因为封装了许多概念, 用户使用简单的接口即可进行测试. 但有如下缺点:

  1. 只能测试有限序列.
  2. 由于它的工作原理是阻塞当前线程并锁定 run loop, 因此如果被测 Observable 使用时间操作符调度(比如延迟 3 秒)事件, 则实际测试过程中也会等这么长的时间.
  3. 无法对事件产生时候的时间戳进行测试(因为它无法记录事件产生时间)
  4. 不适用于需要异步输入流的测试, 比如一个流需要另外一个流去触发(trigger).

使用 RxBlocking 可以让测试更简单. RxTest 则提供对于流生成的精确控制.

参考


RxSwift 的单元测试
https://blog.rayy.top/2020/11/15/2020-11-15-rxswift-unittest/
作者
貘鸣
发布于
2020年11月15日
许可协议