使用TestScheduler测试反应式代码

TL; DR

异步反应性函数,例如Observable.interval()Observable.timer()或您自己的异步反应性函数,可以通过沿时间轴移动被测对象来使用RxTest.TestScheduler进行测试。 真正忙碌的人可能会立即深入那里的代码:

vadimue / RxLocation
RxLocation –反应性使用CoreLocation github.com

简单样本

我们都写测试,不是吗? 😏有时我们甚至使用TDD。 通常,我们将Observables用于多个异步操作。 但是用单元测试来测试它们可能很棘手。 如果我们不编写DispatchQueue.main.asyncAfter()wait(for: [expectation], timeout: 60)异步测试将失败。 否则,我们可能将Rx用于同步操作,这就是为什么我们不会在执行等待时遇到问题。

例如,让我们看一个测试,该测试涵盖了当用户点击单元格时打开带有详细说明的页面。 该测试同步运行,这就是为什么它呈绿色的原因。

与时间有关的要求

有时需要描述测试中与时间相关的要求。 如果您需要测试发送API请求并同时显示UIActivityIndi​​cator怎么办? 还是基于某些延迟下载数据? 测试可能需要一段时间才能运行,因为它们必须等待操作完成。 显然,我们旨在加快运行测试的速度,因为我们经常运行测试并且不想浪费时间。

那么我们应该如何为这些情况编写测试? 我建议从业务需求入手。 假设您创建了一个用于监视用户位置的应用程序:

  1. 您可以使用CoreLocation框架;
  2. 实现CLLocationManagerDelegate并使之具有响应性(重用来自官方存储库CLLocationManager + Rx.swift和RxCLLocationManagerDelegateProxy.swift的扩展);
  3. 使用这些扩展创建单独的服务;
  4. 之后获取微小的LocationServiceProtocol的实现。

方法location()返回原始CLLocation对象序列。 在当前的实现中,它们每秒以最简单的配置到达。

那么,需求呢?

位置数据传输应该是周期性的,取决于运行和以下规则:

  • 传输必须至少每分钟执行一次(静止),并且位置变化超过10 m;
  • 传输的频率不应超过每10秒一次。

为了使代码简单(嗨,SRP!),我创建了单独的类,该类将过滤虚拟位置序列。

TrackingService符合该协议:

让我们从描述第一个要求开始: 传输必须每分钟至少执行一次。 要遵循TDD,我们应该首先编写测试。 为此创建并设置XCTestCase类:

时间旅行

现在我们可以编写我们的第一个测试。 这是最终版本。 一分钟后,我将详细解释。

  1. 首先,我们使用TestScheduler创建一种特殊的Observable类型并安排时间;
  2. 然后,我们将locationService.track()存根;
  3. 我们创建了两个变量: resultsubscription用于将已过滤的对象从服务中获取到Subject中,并可以比较事件;
  4. 我们订阅了track()方法并将结果保存到subscription
  5. 行刑:时间旅行! 我们通知TestScheduler ,已经过了一定的时间,并且此时确实需要执行什么操作。 我们需要处理subscription以确保此刻之后不会引发任何事件;
  6. 调用方法start()使一切正常;
  7. 将发生的事件与我们的期望进行比较;
  8. 时差为60秒* 10倍+小于60秒的小间隔。

现在让我们尝试为该测试编写代码:

操作员flatMapLatest()为我们完成了所有工作。 对于每个发出的值,我们启动计时器函数interval()并在60秒后从allLocation序列中获取最后一个值。 如果新的值在60秒之前到达allLocations序列,则flatMapLatest()会自动退订前一个计时器并启动一个新计时器。 因此,我们自动摆脱了计时器资源的手动处理,并防止了内存泄漏。 想象一下如果没有Reactive Extensions ,相同的代码会是什么样子,这真是令人恐惧!

让我们进行测试。 而且…失败了。 问题是我们必须等待60秒才能重复第一个发出的值。 测试中有10个。 为什么会这样呢? 我们在测试中仔细考虑了这一点,并将TestSchedulerscheduleAt()函数一起使用。 是的,但是Observable.interval()将如何知道这一点? 它应该使用TestScheduler的实例,而不是SerialDispatchQueueScheduler 。 例如,您可以将其注入服务的构造函数中,并在以后使用它创建计时器。

再次运行测试,现在是绿色! 另外,我已经实现了其余需求并对其进行了全部测试。 您可以在TrackingServiceTests和TrackingService上找到代码。

结论

我们编写了令人难以置信的测试,该测试完全涵盖了业务需求。 XCTest并不是测试的好工具。 但是使用RxTest,我们可以控制几乎所有内容-这是管理复杂的异步代码的好方法。