概要
非同期処理・時間を含むロジックのテストは、
しばしば「実行待ち」「タイミングのズレ」「偶発的成功」などの問題を招く。
RxJSはこの問題を根本から解決する手段として、「仮想時間」によるマーブルテストを導入している。
本稿では、Rxにおけるテストの哲学とマーブル記法、そして時間を制御可能にする設計的恩恵を構造的に解説する。
1. 非同期のテストが難しい理由
- 時間軸に依存(
setTimeout
,debounceTime
,delay
…) - 外部要因に左右されやすい(ネットワーク、UIイベント等)
- 再現性の低いバグが潜む
→ 実時間ベースでは再現性が不安定になりやすい
2. マーブルテストとは何か?
「仮想時間上のストリームの動きを、記号で表現して検証するテスト手法」
--a--b---c--|
このようなマーブル文字列で、Observableの挙動を視覚的・構造的に定義できる。
-
-
: 仮想時間の1フレーム -
a, b, c
: nextで発行された値 -
|
: complete -
#
: error
3. TestScheduler
を用いたマーブルテストの構造
import { TestScheduler } from 'rxjs/testing'
import { map } from 'rxjs'
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected)
})
testScheduler.run(({ cold, expectObservable }) => {
const source = cold('--a--b--c--|')
const expected = '--x--y--z--|'
const result = source.pipe(
map((value) => value.toUpperCase())
)
expectObservable(result).toBe(expected, { x: 'A', y: 'B', z: 'C' })
})
-
cold()
:Observableの仮想定義 -
expectObservable()
:その出力結果をマーブル記法で検証 - 時間と値を構造的に検証できる
4. run()
モードによる時間の抽象化
TestScheduler.run()
によって、すべての演算子(debounceTime
, delay
, switchMap
など)も仮想時間で動作させられる。
これにより:
- テスト高速化(リアル時間不要)
- 時間依存バグの再現
- タイミングによる競合のデバッグが容易
5. 応用:debounceTime のマーブルテスト
testScheduler.run(({ cold, expectObservable }) => {
const input = cold('a--a---a----|')
const expected = '--- --- ----(a|)'
const result = input.pipe(debounceTime(5))
expectObservable(result).toBe(expected)
})
→ debounceTime
によって 最後の1つだけが遅れて出力されることを検証
6. 効果的なRxテスト戦略
パターン | アプローチ |
---|---|
時間が関与する処理 |
TestScheduler.run() + マーブル記法 |
非同期API呼び出し | ストリームをstub化 / switchMap の挙動検証 |
UIイベントの反映 |
fromEvent 相当の cold observable で再現 |
よくある誤解と対策
❌ マーブル記法は覚えにくい
→ ✅ 最初はそう感じるが、視覚的な時系列表現が頭に残りやすく、ロジックの理解に直結する
❌ Rxのテストは遅くて書きにくい
→ ✅ 仮想時間ベースであれば、超高速・高再現性のテストが可能
❌ 普通のテスト(Jest)で十分では?
→ ✅ Promise
ベースのテストでは、タイミングに依存するUI処理や連続API呼び出しの制御が困難
結語
非同期はテストが難しい──
それは「時間」と「順序」という“見えない変数”が常に絡むからである。
Rxのマーブルテストは、
その見えない流れを構造として可視化し、設計・検証の対象へと変換する技術である。
リアクティブなテスト戦略とは、
“非同期の振る舞いを仮想空間に引き上げ、抽象的に設計し、再現可能に制御するための思想である。”