Rx-Testingの使い方

Rxを使ったプログラミングは楽しいのですが、時間に依存してたり非同期だったりするのでテストを書こうと思うとなかなか難しい。

そこでReactive Extensionsでは、テスト用のクラスをいくつか用意してくれています。

テスト用のクラスを利用するためには、NuGetからRx-Testingを入れるか、インストーラな人はMicrosoft.Reactive.Testingを参照設定に追加します。

ITestableObserverとITestableObservable

Rxでプログラミングしている時のテスト対象は、何らかのIObserverやIObservableになると思います。

IObservableのテストをしたいときは、いつどんな通知が行われたかを確認したいですし、IObserverをテストしたいときは、任意のタイミングで通知できるIObservableが欲しい。

そのために、Rx-TestingではITestableObserverとITestableObservableという2種類のインタフェースと、その実装クラスであるMockObserver、HotObservable、ColdObservableというクラスを用意しています。

これらはinternalクラスなので、インスタンスはTestSchedulerから作ることになります。

例えば、MockObserverインスタンスは以下のように作ります。

var testScheduler = new TestScheduler();
var mockObserver = testScheduler.CreateObserver<int>();

IObservableのテスト

IObservableをテストしたい場合は、テスト対象のIObservableに対して、MockObserverをSubscribeします。

var testScheduler = new TestScheduler();
var mockObserver = testScheduler.CreateObserver<int>();
IObservable<int> testTarget = ・・・
testTarget.Subscribe(mockObserver);

この状態でtestTargetから通知が行われると、MockObserverのMessagesに通知結果が蓄積されます。

蓄積されるのは、Recorded>という型で、通知の時間とその値を保持しています。
また、どんな通知が行われたかは、Notificationのサブクラスの型(Notificatioを継承したOnNextNotification/OnErrorNotification/OnCompletedNotification)で判断することができます。


通知結果を確認するためには、ReactiveAssertのAreElementsEqualを使います。

しかし、期待する結果を用意するために、Recorded>のインスタンスを作るのはちょっと面倒です。
そこで、ReactiveTestクラスに、Recorded>を簡単に作るためにOnNext/OnError/OnCompletedというFactoryメソッドが用意されています。

テストクラスでReactiveTestを継承すると使いやすいですね。

というわけで、IObservableのテストは以下のように書くことができます。(テストフレームワークにはMSTestを使っています。)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using Microsoft.Reactive.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class RxTest1 : ReactiveTest
{
    [TestMethod]
    public void Observableのテスト()
    {
        var testTarget = new Subject<int>(); //テストしたい対象

        var testScheduler = new TestScheduler();
        var mockObserver = testScheduler.CreateObserver<int>();
        
        // テスト対象を監視する。
        testTarget.Subscribe(mockObserver);

        // 時間を進めつつ通知を行う
        testTarget.OnNext(1);
        testScheduler.AdvanceBy(100);
        testTarget.OnNext(2);
        testScheduler.AdvanceBy(100);
        testTarget.OnNext(3);
        testScheduler.AdvanceBy(100);
        testTarget.OnCompleted();

        // 通知結果の確認
        ReactiveAssert.AreElementsEqual(
            mockObserver.Messages,
            new[] { OnNext(0, 1), OnNext(100, 2), OnNext(200, 3), OnCompleted<int>(300) });
    }
}

時間の操作

TestSchedulerでは、AdvanceByやAdvanceToメソッドを使って、自由に時間を進めることができます。

あまり現実的ではないですが、例えば以下のように1時間に一回通知を行うようなクラスがあったとしましょう。

public class PeriodicObservable : IObservable<long>
{
    private readonly IScheduler _scheduler;

    public PeriodicObservable (IScheduler scheduler)
    {
        _scheduler = scheduler;
    }

    public IDisposable Subscribe(IObserver<long> observer)
    {
        return Observable.Interval(TimeSpan.FromHours(1),_scheduler).Subscribe(observer);
    }
}

通知されるまでスリープしていては、テストに時間がかかりすぎてしまいます。

そこで、TestSchedulerを使って時間を操作してしまいましょう。すると、上記のようなクラスのテストも一瞬で実行することができます。

[TestClass]
public class RxTest2 : ReactiveTest
{
    [TestMethod]
    public void 時間がかかるObservableのテスト()
    {
        var testScheduler = new TestScheduler();
        var mockObserver = testScheduler.CreateObserver<long>();

        var testTarget = new PeriodicObservable(testScheduler);

        testTarget.Subscribe(mockObserver);

        // 時間を5時間進める
        testScheduler.AdvanceBy(TimeSpan.FromHours(5).Ticks);
        
        // 5回通知が発生したことを確認
        Assert.AreEqual(mockObserver.Messages.Count, 5);
    }
}

IObserverのテスト

一方、IObserverのテストをしたいときは、任意のタイミングで通知してくれるHotObservable、ColdObservableを使います。
それぞれTestSchedulerのCreateHotObservableとCreateColdObservableメソッドで、通知したい時間と値を指定して作ることができます。
HotとColdの違いについてはneue cc - Reactive Extensions for .NET (Rx) メソッド探訪第7回:IEnumerable vs IObservableが参考になります。

これらのクラスは、TestSchedulerの時間を進めると、指定したタイミングで通知してくれます。
当然ですが時間は進めることしかできず、過去に戻ろうとすると例外が発生します。

IObserverのテストは、こんな感じになります。

[TestClass]
public class RxTest3 : ReactiveTest
{
    [TestMethod]
    public void Observerのテスト()
    {
        var testScheduler = new TestScheduler();

        var list = new List<int>();
        var testTarget = Observer.Create<int>(x => list.Add(x));

        // 通知したい時間と値をセットしておく。
        var hotObservable = testScheduler.CreateHotObservable(OnNext(0, 4), OnNext(100, 5), OnNext(200, 6));

        var d = hotObservable.Subscribe(testTarget);

        // 時間を進めて通知を発生させる
        testScheduler.AdvanceBy(50);
        ReactiveAssert.AreElementsEqual(list, new[] { 4 });

        testScheduler.AdvanceBy(100);
        ReactiveAssert.AreElementsEqual(list, new[] { 4, 5 });

        testScheduler.AdvanceBy(100);
        ReactiveAssert.AreElementsEqual(list, new[] { 4, 5, 6 });

        d.Dispose();
    }
}

これでRx開発も捗りますね!