让我检查我的调度程序

为RxSwift编写测试可能很棘手。 RxSwift的一项核心功能是默认情况下所有操作都是异步发生的。 如果您正确连接了所有可观察对象和观察者,则您的应用程序将按预期运行。 不幸的是,异步代码很难进行单元测试,因为您必须处理近似和可能。 但是,如果我们正确地组织代码,则测试Rx代码将变得非常简单。

通常,RxSwift与MVVM结合使用,因此让我们编写一个ViewModel进行测试。 当我在iOS中指MVVM时,指的是一种体系结构,其中代码按数据传输类型(模型),视图(UIView和UIViewController子类)和ViewModels大致划分。 视图主要用于在屏幕上绘制,模型没有逻辑,并且可以从一个地方到另一个地方传送数据。 ViewModel用于获取模型并以使View简单的方式对其进行转换。 尽管我们的体系结构只有四个字母和三个组成部分,但这并不意味着我们仅限于三种对象。 这些类型中的每一种都可以由名称和责任不同的任何数量的类型支持。 但是,定义整个iOS应用程序应如何设计其体系结构超出了本博客文章的范围,因此让我们继续使用ViewModel。

假设我们有一个应用程序供用户发布他们喜欢的乐器的图片。 在这个应用程序中,我们希望用户能够互相关注。 当您关注用户时,将看到一个显示以下内容的视图:

  1. 您有多少个追随者
  2. 最后追踪的对象

因此,我们的应用程序需要一种创建关系的方法,在该关系中用户跟随另一个用户,然后我们收集该数据并将其显示在屏幕上。

让我们从考虑网络开始。 假设我们的后端为用户提供了一个不错的RESTful接口。 api / v1 / followers /将使用我们的一系列用户响应GET请求。 该端点还将允许客户端将用户放入用户并创建新的关注者。

对于我们的iOS客户端,我们可能需要使用URLSession发出网络请求。 不必担心URLSession如何工作的细节,我们如何制作一个协议来包装网络调用。 这样,我们可以为生产实现真正的网络类型,但在测试中将其保留。

美丽! 现在我们知道了网络层的外观,让我们考虑一下ViewModel。 我们需要一种跟踪用户的方法。 让我们从这里开始。

这是一个很好的开始! 现在,我们有了一个可以跟随其他用户的对象! 更好的是,由于我们指定了调度程序,因此可以通过将网络调用移至后台队列来防止UI挂起。 如果您不熟悉调度程序,则它们是决定可观察对象的工作将在哪里运行的类型。

尽管这是一个很好的第一步,但是当您仔细观察时,并没有真正好的方法来对该对象进行单元测试。 这是个“火上浇油”的电话,因此,作为消费者,没有办法知道跟踪是否成功。 最重要的是,因为我们在该对象内创建了操作队列,所以即使我们知道要查找的内容,也无法确定它何时真正发生。 让我们解决这个问题。

我们还有两个要求:

  1. 您有多少个追随者
  2. 最后追踪的对象

由于这些要求都使用来自同一端点的数据,因此我们应该能够将这些实现为对来自followers()方法的可观察输入的转换。 让我们将两个可观察对象连接在一起,以便在您请求关注者时,如果响应成功,则请求更新的关注者列表。

该对象感觉更适合测试,我们提供了输入(用户),并且我们应该能够观察到输出(新计数和最新关注者)。

为了隔离我们正在测试的内容,我们将注入一个模拟的FollowerNetworkLayer,它将返回任意的测试数据。 自创建了FollowerNetworkLayer协议以来,我们可以创建一个接受静态数据的结构并将其返回以进行测试。 我们不在乎测试我们的网络层,我们可以单独进行测试。 当前,我们仅在测试要求我们的ViewModel跟随用户时,它会正确返回预期数据。 它完全需要网络层的事实与我们的实际目的相切。

在这里,我们在Observable上使用.just(_ :)静态方法来创建我们的Observable。 .just(_ :)将创建一个可观察的项目,该项目将在订阅后立即返回其值。 这对于只需要一个值的情况非常有用,但是如果流中需要多个项目,则它会开始下降。 不过,这不是问题,因此让我们继续编写测试用例。

不完美且冗长的测试。

停在这里是完全可以接受的。 我们有与大多数系统隔离的测试。 它们将准确反映系统的运行状况。 如果可以接受,那就去吧! 但是RxTest为我们提供了一些工具,这些工具将使您的测试更加可预测,我想您会喜欢它的。

与其让异步代码来决定如何编写测试,不如让我们尝试使ViewModel更可预测。 如果我们回想起FollowUserViewModel,我们将使用OperationQueue创建自己的调度程序。 与其让ViewModel自己做这件事,不如让我们将责任推给消费者。 这样,我们的ViewModel可以配置为可预测我们的代码在哪里运行。 我们对此类的修改应如下所示:

依赖注入以使用新的Scheduler配置ViewModel

现在,实例化FollowUserViewModel的任何人都可以指示其在哪里工作。 对于我们的ViewController或View,这可能是一个后台调度程序,但是对于我们的测试,请尝试使用一些新的东西。

这是TestScheduler闪耀的地方。 TestScheduler是一种调度程序,它以自己的测试时间运行,而不是使用实际秒数。

根据RxTest文档

测试调度程序实现与本地计算机时钟分开的虚拟时间。 这样可以尽快运行模拟并证明所有事件均已处理。

因此,TestSchedulers之所以快速,是因为他们按自己的时间而不是实时地工作。 这意味着,与时间相关的运算符(如反跳和延迟)在TestScheduler上运行将不会在测试中产生任何明显的延迟。

TestSchedulers还允许用户创建测试可观察对象,用户可以在其中确定确切的事件以及观察者何时接收到这些事件。 这意味着您可以指定每个事件之间经过的测试时间。 这对于测试速率限制器和其他基于时间的运算符等至关重要。

使用TestScheduler,可以决定创建一个热或冷可观察物,以模拟生产中该类型的可观察物。 通过这种方式创建的可观察对象甚至可以指定错误或已完成的事件。

TestSchedulers也可以用于创建可绑定到可观察对象的TestObserver。 TestObservers允许您检查它们接收了多少个事件以及事件的值。 可以这样做是因为,当您创建一个调度程序并连接所有可观察对象和观察者时,您会调用start(),它将在调度程序执行所有事件时阻塞。 调度程序完成后(由初始化时的超时值确定),您可以看到它在运行时发生的所有情况。

因此,有了这个新工具,让我们更新测试以注入TestScheduler,而不是使用PublishSubjects&wait。

我们可以为之骄傲的测试

我们能够为每个可观察项消除几行代码,因为我们不再需要为进入PublishSubjects中的事件创建订阅。 而是将观察者从FollowUserViewModel绑定到两个TestObserver。 然后,一旦运行了调度程序,就检查测试观察者收到的事件。

其次,我们能够消除我们的wait(_:for :)调用! 现在,所有工作都在我们的封闭环境中运行,我们无需担心测试完成后正在运行的业务代码!

这种方法的另一个好处是,由于我们的测试是在模拟测试时间内进行的,因此我们绝不会遇到与wait(for:timeout :)调用相关的超时。 这看起来似乎不是一个巨大的胜利,但是如果您有很多都有超时的测试,则可以在发生故障的情况下极大地缩短测试时间。

使用TestSchedulers时要注意的一件事是,它们在无限或长期运行的订阅中表现不佳。 我只看到这是有关重复计时器的问题。 如果ViewModel经常有一个计时器来执行操作,则必须找到一种方法来指示流将以某种方式结束。 否则,调度程序将在计时器完成之前超时。

使任何类型在可观察对象上执行工作时,在初始化时注入调度程序会很有帮助。 当涉及到对象的行为时,它们是难题的关键部分。 能够控制它使测试方式比典型的异步代码的猜测和超时要容易得多。

  • https://github.com/ReactiveX/RxSwift/blob/master/Documentation/UnitTests.md
  • https://github.com/ReactiveX/RxSwift/issues/1100
  • https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md#builtin-schedulers
  • http://reactivex.io/documentation/operators/observeon.html
  • http://reactivex.io/documentation/operators/subscribeon.html