Help us understand the problem. What is going on with this article?

RxSwiftにおけるTestSchedulerの解像度について

More than 1 year has passed since last update.

はじめに

TestSchedulerのresolutionについて日本語の説明がないので自分の理解で書いてみます。
結論を言うとresolutionはテスト時に現実時間を仮想時間に変換することができるのでそのために使っています。

というか英語の説明もたいして無いので、私の説明に間違いがあるかもしれません。
そういう場合はコメント欄で教えていただけると助かります。

参考に次の記事を読んでみても私のガッツが足りないのでイマイチぴんとこないんですよね。

SwiftのRxSwiftでのテストコードをRxBlockingとRxTestを使って導入するチュートリアル
https://qiita.com/tamappe/items/68ee916e926b4e129025

resolutionは何に使える?

例えば.debounce(0.3)などで時間を指定した際、
それをテストするなら

  • 0.3秒以内にイベントを発火(0.2秒後とか)
  • 0.3秒後よりちょっと後にイベント発火(0.4秒後とか)

してみると思います。境界テストをしたいわけですよね。
だけどRxTestのRecorded<Value>構造体はIntでしか仮想時間を設定できない。
だから仮想時間1秒後とかで指定してしまう。

  • 仮想時間0にイベントを実行
  • 仮想時間1でイベントを実行

仮想時間0から1の間に仮想時間0.3のしきい値を超えるのでテストはパスするでしょう。
でも、そうなると境界を捉えられていない気がする。
例えば0.3と設定すべき値なのに0.4と設定してしまっていてもテストはパスするわけですし。

そこでresolutionというのが使えるんだと思うんです。
resolutionによって現実時間の0.3を3秒にできればいい。
そうしたらIntで仮想時間2秒後と仮想時間4秒後にイベント発火させればいい。

RxSwiftのTestSchedulerクラスを見てみます

    /**
     Creates a new test scheduler.

     - parameter initialClock: Initial value for the clock.
     - parameter resolution: Real time [NSTimeInterval] = ticks * resolution 
     - parameter simulateProcessingDelay: When true, if something is scheduled right `now`, 
        it will be scheduled to `now + 1` in virtual time.
    */
    public init(initialClock: TestTime, resolution: Double = 1.0, simulateProcessingDelay: Bool = true) {}

Real time [NSTimeInterval] = ticks * resolutionのticksってなんなんだよちくしょー。

ということでもう少しコードを見てます。

https://github.com/ReactiveX/RxSwift/blob/master/RxTest/Schedulers/TestSchedulerVirtualTimeConverter.swift

// Converter from virtual time and time interval measured in `Int`s to `Date` and `NSTimeInterval`.
public struct TestSchedulerVirtualTimeConverter : VirtualTimeConverterType {
    /// Virtual time unit used that represents ticks of virtual clock.
    public typealias VirtualTimeUnit = Int

    /// Virtual time unit used to represent differences of virtual times.
    public typealias VirtualTimeIntervalUnit = Int

VirtualTimeUnitのコメントを読むと、

Virtual time unit used that represents ticks of virtual clock.

仮想時間の「目盛り(ticks)」の表現に使われる単位、がつまりIntなんだよと。
ticksは名詞だけど、前述した式のticksはおそらくticks of virtual clockを省略してたんでしょということになります。つまり

Real time [NSTimeInterval] = ticks of virtual clock * resolution

ticks of virtual clockをもうこれ仮想時間ということで考えるよー。

三度リファレンスにある式に注目してticksを仮想時間に置き換えます。

Real time [NSTimeInterval] = 仮想時間 * resolution

んでReal timeに0.3を入れて、仮想時間を3にして式に入れてみます

0.3 = 3 * resolution

こうなると

0.1 = resolution

になった!
ということでresolutionを0.1にすることで現実時間0.3を仮想時間3にすることができそう。

繰り返しになりますが、
resolutionは現実時間を仮想時間に変換しテストコードを実行しやすくするために(自分は)使います。

試しにRxSwiftのライブラリにあるPlaygroundで次のようなコードを書いて.debounce(0.3)について想定通りか試します。

検証するコード書いてみる

import RxSwift
import RxCocoa
import RxTest

example("debounceとresolution") { // これはRxのplayground用なのでなくてもいいです
    let resolution = 0.1
    let scheduler = TestScheduler(initialClock: 0, resolution: resolution)

    let observable = scheduler.createHotObservable([
        Recorded.next(1, "000"), // このあと仮想時間3秒後(=4)にイベントなし
        Recorded.next(5, "001"),
        Recorded.next(6, "010"),
        Recorded.next(7, "111"), // このあと仮想時間3秒後(=10)にイベントなし
    ])

    let observer = scheduler.createObserver(String.self)
    observable
        .debounce(0.3, scheduler: scheduler)
        .bind(to: observer)

    scheduler.start()

    print(observer.events) // [next(000) @ 4, next(111) @ 10]
}

想定通りでした。

ちなみに.debounce(n)のnを変更してみると更に確認ができます。

  • .debounce(0.1)にすると[next(000) @ 2, next(111) @ 8]になる
  • .debounce(0.5)にすると[next(111) @ 12]になる

私はresolutionについて「実時間を仮想時間に変換するための値」という理解でやってますけど、詳しいリファレンスとかresolutionが他の用途に使うものなのかを知ってる方は教えていただければ幸いです。

なぜこんなことをやる必要があるのかを考える

  • 仮想時間は何のため?
    • イベントの時間に対して現実時間を経過させずに検証できるようにする
    • 仮想時間の検証のためには比較が使われる
      • 比較しやすさを考えシンプルさのためには単位はIntを使う
  • 仮想時間の単位はIntになると浮動少数なリアル時間を観測しようとすると精度を懸念すべき
    • 現実時間をIntの仮想時間として変換する際に解像度(resolution)の概念によって変換する値を変えればいい

まとめ: 仮想時間の変換

リアル時間[sec] = 仮想時間[ticks] * 解像度(resolution)

という式からリアル時間を仮想時間に変換する式を使って整数値にしておきましょうってことですかね

リアル時間[sec] / 解像度(resolution) = 仮想時間[ticks]

  • リアル時間1秒で解像度を1.0とした場合、仮想時間1となる
  • リアル時間1秒で解像度を0.1とした場合、仮想時間10となる
  • リアル時間0.3秒で解像度を0.1とした場合、仮想時間3となる

こうすることで

  • 0.3秒の.debounce(0.3, scheduler: scheduler)をテストしたいとき
    • 解像度を0.1にする
      • リアル時間0.1秒は仮想時間3が経過することになる
    • 仮想時間1にイベントを発火してから仮想時間4以内にイベントが発火しなければ
      • debounceの結果が購読される

おまけ: Converterで実際に計算しているコードを見てみる

TestSchedulerVirtualTimeConverter構造体のコードを見てみます。

https://github.com/ReactiveX/RxSwift/blob/master/RxTest/Schedulers/TestSchedulerVirtualTimeConverter.swift

リアル時間[sec] = 仮想時間[ticks] * 解像度(resolution)

になってるかな

    /// Converts from virtual time interval to `NSTimeInterval`.
    ///
    /// - parameter virtualTimeInterval: Virtual time interval to convert to `NSTimeInterval`.
    /// - returns: `NSTimeInterval` corresponding to virtual time interval.
    public func convertFromVirtualTimeInterval(_ virtualTimeInterval: VirtualTimeIntervalUnit) -> RxTimeInterval {
        return RxTimeInterval(virtualTimeInterval) * _resolution
}

これを見るとvirtualTimeIntervalが仮想時間で解像度が_resolution
ticksっていう言い方はもうここでは使わないぜ、という感じになってるのが若干気になるところですが、式通りと言えそうです。

yimajo
株式会社キュリオシティソフトウェアの代表です。iOSアプリを作っています。最近はCombine frameworkガイドブック / RxSwift研究読本などを書いてます。
https://swift.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away