概要
非同期処理・時間を含むロジックのテストは、
しばしば「実行待ち」「タイミングのズレ」「偶発的成功」などの問題を招く。
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のマーブルテストは、
その見えない流れを構造として可視化し、設計・検証の対象へと変換する技術である。
リアクティブなテスト戦略とは、
“非同期の振る舞いを仮想空間に引き上げ、抽象的に設計し、再現可能に制御するための思想である。”