Rx + MolesによるC#での次世代非同期モックテスト考察

最近、妙にテストブームです。Chaining Assertionを作ったからですね。ライブラリドリブンデベロップメント。とりあえずでも何か作って公開すると、その分野への情報収集熱に火がつくよね。そしてテスト厨へ。さて、ユニットテストで次に考えるべきは、モックの活用。C#でモックといえばMoqが評価高い。メソッドチェーンとExpression Treeを活かしたモック生成は、なるほど、良さそうです。読み方も可愛いしね。もっきゅ。もっきゅ。

というわけでスルーして(えー)Molesを使いましょう。Microsoft Research謹製のモックフレームワークです。PexとのセットはMSDN Subscriptionが必要ですが、MolesのみならばFreeです。VS Galleryに置かれているので、VSの拡張機能マネージャーからでも検索に引っかかります。

Moles。Pex and Molesとして、つまりPex(パラメータ自動生成テスト)のオマケですよねー、と考えていたりしたりした私ですが(実際、Pexがこの種のモックシステムを必要とする、という要請があって出来た副産物のよう)、これがオマケだなんてとんでもない!アセンブリ解析+DLL自動生成+ILジャックという、吹っ飛んだ発想による出鱈目すぎる魔法の力でモック生成してしまうMolesは、他のモックフレームワークとは根源的に違いすぎる。

Molesとは何か。既存のクラスの静的/インスタンスメソッドやプロパティの動作を、自由に置き換えるもの。既存のクラスとは、自分の作ったものは勿論、.NET Frameworkのクラスライブラリも例外ではありません。Console.WriteLineやDateTime.Now、File.ReadAllTextなども、そのままに乗っ取ることが可能です。PublicもPrivateも、どちらでも乗っ取れます。

しかも使うのは簡単。往々に強力なものは扱いも難しくなってしまうものですが、常識はずれに強力な魔法が働いている場合は、逆に非常に簡単になります。対象となるメソッドにラムダ式を代入する。それだけ。moqなどよりも遥かに簡単。

Molesを使う

日本語での紹介はMoles - .NETのモック・スタブフレームワーク - Jamzzの日々に、また、MSRのページのDocumentationにLevel分けされた沢山のドキュメントが用意されているので(素晴らしい!Rxも見習うべし!)、そちらに目を通せば大体分かると思われます。

とりあえず使ってみましょう。Molesをインストールしたら、テストプロジェクトを作って、参照設定を右クリックし、Add Moles Assembly for mscorlibを選択。

するとmscorlib.molesというファイル(中身はただのXML)が追加されます。そして、とりあえずビルドするとMicrosoft.Moles.Framework, mscorlib.Behavior, mscorlib.Molesが参照設定に追加されます。つまり、mscorlibが解析され、モッククラスが自動生成されました!mscorlib以外のものも生成したい場合は、参照設定の対象dll上で右クリックし、Add Moles Assemblyを選べば、.molesが追加されます。なお、解析対象が更新されてHoge.Molesも更新したい、という場合はリビルドすれば更新されます(逆に言えばリビルドしないと更新されないため、コンパイルは通るものの実行時エラーになります)。また、もし追加したことによって何かエラーが出る場合(VS2010 SP1で私の環境ではSystem.dllでエラーが発生する)は、.molesの対象アセンブリの属性にReflectionOnly="true"も記載すると回避できることもあります。

では簡単な例を。

// mscorlibに含まれる型の場合のみ、Molesで乗っ取りたい型を定義しておく必要があります
// 定義なしで実行すると、この型定義してね、って例外メッセージが出るので
// それが出たらコピペってAssemblyInfo.csの下にでも書いておけばいいんぢゃないかな
[assembly: MoledType(typeof(System.DateTime))]

[TestClass]
public class UnitTest1
{
    // 現在時刻を元に"午前"か"午後"かを返すメソッド
    public static string ImaDocchi()
    {
        return (DateTime.Now.Hour < 12) ? "午前" : "午後";
    }

    // HostType("Moles")属性を付与する必要がある
    [TestMethod, HostType("Moles")]
    public void TestMethod1()
    {
        // ラムダ式で置き換えたいメソッドを定義する
        // プリフィックスMが自動生成されているクラス、
        // サフィックスGetはプロパティのgetの意味

        MDateTime.NowGet = () => new DateTime(2000, 1, 1, 5, 0, 0);
        ImaDocchi().Is("午前");

        MDateTime.NowGet = () => new DateTime(2000, 1, 1, 15, 0, 0);
        ImaDocchi().Is("午後");
    }
}

お約束ごと(属性付与)が若干ありますが、エラーメッセージで親切に教えてくれるので、そう手間もなくMoles化出来ます。モック定義自体は何よりも簡単で、見たとおり、デリゲートで置き換えるだけです。非常に直感的。(IsはChaining Assertion利用のものでAssert.AreEqualです、この場合)

システム時刻に依存したメソッドのテストは、単体テストの書き方として、よく例に上がります。そのままじゃテスト出来ないのでリファクタリング対象行き。メソッドの引数に時刻を渡すようにするか、時刻取得を含んだインターフェイスを定義して、それを渡すとか、ともかく、共通するのは、外部から時刻を操れるようにすることでテスト可能性を確保する。ということ。

Molesを使えば、そもそもDateTime.Now自体をジャックして任意の値を返すように定義出来てしまいます。これは単純な例でしかないので、いくら出来てもそんなことやらねーよ、かもですね。はい。それが良い設計かどうかは別としても、Molesの存在を前提とすると、テスト可能にするための設計方法にも、かなりの変化が生じるのは間違いないでしょう。時に、テスト可能性のために歪んだ設計となることも、Molesで乗っ取れるのだと思えば、自然な設計が導出できるはず。

イベントのモック化

続けてイベントの乗っ取りも画策してみましょう。イベントの乗っ取りは、正直なところ少し面倒です。

// こんな非同期でダウンロードして結果を表示するメソッドがあるとして
public static void ShowGoogle()
{
    var client = new WebClient();
    client.DownloadStringCompleted += (sender, e) =>
    {
        Console.WriteLine(e.Result);
    };
    client.DownloadStringAsync(new Uri("http://google.co.jp/"));
}

[TestMethod, HostType("Moles")]
public void WebClientTest()
{
    // 外から発火出来るように外部にデリゲートを用意
    DownloadStringCompletedEventHandler handler = (s, e) => { };

    // AddHandlerとRemoveHandlerを乗っ取って↑のものに差し替えてしまう
    MWebClient.AllInstances.DownloadStringCompletedAddDownloadStringCompletedEventHandler =
        (wc, h) => handler += h;
    MWebClient.AllInstances.DownloadStringCompletedRemoveDownloadStringCompletedEventHandler =
        (wc, h) => handler -= h;

    // DownloadStringAsyncをトリガに用意したデリゲートを実行
    MWebClient.AllInstances.DownloadStringAsyncUri = (wc, uri) =>
    {
        // DownloadStringCompletedEventArgsはコンストラクタがinternalなので↓じゃダメ
        // handler(wc, new DownloadStringCompletedEventArgs("google!modoki"));
        // というわけで、モックインスタンス作ってしまってそれを渡せばいいぢゃない
        var mockArgs = new MDownloadStringCompletedEventArgs()
        {
            ResultGet = () => "google!modoki"
        };
        handler(wc, mockArgs);
    };

    // 出力はConsole.WriteLineなので、それを乗っ取って、結果にたいしてアサート
    MConsole.WriteLineString = s => s.Is("google!modoki");

    ShowGoogle(); // 準備が終わったので、実行(本来非同期だけど、全て同期的処理に置き換えられてます)
}

ちょっと複雑です。テストしたい処理はDownloadStringCompletedの中ですが、外からこれを発火する手段は、ない。この例だとAddHandlerだけ乗っ取って、直に発火させてもいいのですが、(非同期だけじゃなく)他のイベントの場合でも応用が効くように、正攻法(?)でいきましょう。イベントの発火を自分でコントロール出来るように、まずはAddとRemoveに対し、外部デリゲートに通すよう差し替えます。なお、インスタンスメソッドを乗っ取る場合は.AllInstancesの下にあるインスタンスメソッドを、静的メソッドと同じようにラムダ式で直に書き換えるだけです。非常に簡単。なお、第一引数は必ず、そのインスタンス自身となっていることには注意。

あとは、トリガとなるメソッドがあればそれを(この場合はDownloadStringAsync)通して、そうでない場合(例えばただのボタンクリックとか)なら直にイベントを乗っ取ったデリゲートを発火してやれば完了。で、ここでEventArgsがコンストラクタがprivateなせいで生成出来なかったりというケースも少なくないのですが、それはモックインスタンスを作って、そいつを渡してやるだけで簡単に回避できます。

少し手順が多いですが、「出来る」ということと、まあ流れ自体は分かるしそれぞれは置き換えるだけで複雑じゃない。ということは分かるのではと思います。でも、それでも面倒くさいですよ。ええ、どう見ても面倒くさいです。しかし、このことはReactive Extensionsを使えば解決出来ます。んが、その前にもう一つ別の例を。

APMのモック化

非同期繋がりで、APM(Asynchronous Programming Model, BeginXxx-EndXxx)のモック化もやってみましょう。

// こんな非同期でダウンロードして結果を表示するメソッドがあるとして
public static void ShowBing()
{
    var req = WebRequest.Create("http://bing.co.jp/");
    req.BeginGetResponse(ar =>
    {
        var res = req.EndGetResponse(ar);
        using (var stream = res.GetResponseStream())
        using (var sr = new StreamReader(stream))
        {
            var result = sr.ReadToEnd();
            Console.WriteLine(result);
        }
    }, null);
}

[TestMethod, HostType("Moles")]
public void WebRequestTest()
{
    // Beginでコールバックを呼ぶ、EndでWebResponseを返す
    MHttpWebRequest.AllInstances.BeginGetResponseAsyncCallbackObject =
        (req, ac, obj) => { ac(null); return null; };
    MHttpWebRequest.AllInstances.EndGetResponseIAsyncResult = (req, ar) =>
    {
        return new MHttpWebResponse
        {
            GetResponseStream = () => new MemoryStream(Encoding.UTF8.GetBytes("bing!modoki"))
        };
    };

    MConsole.WriteLineString = s => s.Is("bing!modoki");

    ShowBing(); // 実行
}

イベントよりは少し簡単ですが、BeginとEndの絡み具合は混乱してしまいます。また、HttpWebResponseのダミーを作るのも面倒。

Reactive Extensions

見てきたように、イベントもAPMも、モック化は面倒です。そこで出てくるのがReactive Extensions。RxならばIObservableとして一つにまとまるので、その一点をモック化してしまえばそれだけですむ、しかもダミーのIObservableを生成するのは非常に簡単!というわけで、例を見ましょう。モック化、の前に非同期のRx化と、そのテストを。

// こっち本体

public class Tweet
{
    public string Name { get; set; }
    public string Text { get; set; }

    // 実際やるなら静的メソッドじゃなくて、API操作はまとめて別のクラスで、と思いますが、まあとりあえずこれで
    public static IObservable<Tweet> FromPublicTL()
    {
        var req = WebRequest.Create("http://twitter.com/statuses/public_timeline.xml");
        return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
            .Select(r =>
            {
                // StreamはSilverlightでも同期で書けるので、同期で取得しちゃいます
                using (var stream = r.GetResponseStream())
                using (var sr = new StreamReader(stream))
                {
                    return sr.ReadToEnd();
                }
            })
            .SelectMany(s => XElement.Parse(s).Elements()) // 配列上のものをバラして
            .Select(x => new Tweet // Tweetに変換
            {
                Text = x.Element("text").Value,
                Name = x.Element("user").Element("screen_name").Value
            });
    }
}

// こっちがTest

[TestMethod]
[Timeout(3000)] // Timeoutはテスト全体のオプションで設定してもいいね
public void FromPublicTL()
{
    var tl = Tweet.FromPublicTL().ToEnumerable().ToArray();

    // 20件あって、NameとかTextが
    // 全部空じゃなければ正常にParse出来てるんじゃないの、的な(適当)
    tl.Length.Is(20);
    tl.All(t => t.Name != "" && t.Text != "").Is(true);
}

Twitterのpublic_timeline.xml、つまり認証のかかってない世界中のパブリックなツイートが20件(オプション無しの場合)XMLで取れるAPIを叩いています。RxのFromAsyncPatternを使い、リクエストは非同期。非同期のテストは通常難しい、のですが、Rxの場合はFirstやToEnumerableで簡単にブロックして同期的なものに変換出来るため、それで結果を取って、何食わぬ顔でアサートしちゃえます。

Rxは非同期が簡単にテスト出来てメデタシメデタシ。これはこれで良いのですが、ところでパブリックじゃなくて認証入るものを取るときはどうするの?ストリーミングAPI(ツイートだけじゃなくFavoriteなど色々な形式のXMLが届く)を試したいけど、誰かがFavoriteつけるまで待機とか、テストに不定な時間がかかるものはどうするの?などなどで、本物のウェブ上のデータをテストで毎回取ってくるのは大変です。また、誤ったデータが流れてきた/サーバーが応答不能状態な場合などの例外処理のテストは、通常では出来ないですね?

そこで、モック。ウェブからじゃなくてモックがダミーのデータを返せばいいわけだ。そして改めてFromPublicTLメソッドを見ると「データ取得」と「データパース」の二つを行っている。なので、ここはその二つに分けて、後者の「データパース」がモックでテスト出来るようにしてやりましょう。

public class Tweet
{
    public string Name { get; set; }
    public string Text { get; set; }

    private static IObservable<String> GetRawPublicTL()
    {
        var req = WebRequest.Create("http://twitter.com/statuses/public_timeline.xml");
        return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
            .Select(r =>
            {
                using (var stream = r.GetResponseStream())
                using (var sr = new StreamReader(stream))
                {
                    return sr.ReadToEnd();
                }
            });
    }

    public static IObservable<Tweet> FromPublicTL()
    {
        return GetRawPublicTL()
            .SelectMany(s => XElement.Parse(s).Elements())
            .Select(x => new Tweet
            {
                Text = x.Element("text").Value,
                Name = x.Element("user").Element("screen_name").Value
            });
    }
}

リファクタリングというほど大仰なものでもなく、メソッドチェーンのうちのネットワークアクセス部分をprivateメソッドとして切り出しただけです。Rxは、メソッドチェーンの一つ一つが独立しているので、切った貼ったが簡単なのもメリット。では、このprivateメソッドをMolesで差し替えてしまおう!

[TestMethod, HostType("Moles")]
public void FromPublicTLMock()
{
    // これは省略した文字列ですが、実際は取得したXMLをファイルに置いて、それを読み込むといいかも
    var statuses = @"
        <statuses>
            <status>
                <text>Hello</text>
                <user>
                    <screen_name>neuecc</screen_name>
                </user>
            </status>
            <status>
                <text>Moles</text>
                <user>
                    <screen_name>xbox99</screen_name>
                </user>
            </status>
        </statuses>
        ";

    // 本来ネットワーク取得のものを、たった一行でただのシーケンスに置き換える
    MTweet.GetRawPublicTL = () => Observable.Return<string>(statuses);

    var tl = Tweet.FromPublicTL().ToEnumerable().ToArray();

    tl.Length.Is(2);
    tl[0].Is(t => t.Name == "neuecc" && t.Text == "Hello");
    tl[1].Is(t => t.Name == "xbox99" && t.Text == "Moles");
}

これだけです。データ用意は別として、モックへの差し替えはたった一行書いただけ。既存のコードに一切手を加えず、こんなにも簡単にモックへの置き換えが可能だなんて、わけがわからないよ。

理由として、Rxの持つ非同期もイベントも普通のシーケンスも、全て等しく同じ基盤に乗っている、という性質が生きています。この性質は時に分かりづらさを生むこともありますが、しかしそれ故に絶大な柔軟性も持っていて、その結果、本来非同期処理のものをただのシーケンスに置き換えることを可能にしています。非同期が、イベントがテストしづらいならMolesでただのシーケンスに差し替えてしまえばいい。別段「テストのため」の設計を意識しなくても、Rxで書くということ、それだけで自然にテスト可能な状態になっています。

なんて、さらっと流してしまっているわけですが、この事に気づいた瞬間にこれはヤバい!と悶えました。いや、凄いよ、凄過ぎるよRx + Moles。

Moq vs Moles、あるいは検証のやり方

Molesは非常に強力ですが、ではMoqと、どう使いわけよう?もしくは、全て代替出来てしまう?Molesは純粋な置き換えのみなので、呼び出しの検証はありません。モックとスタブの用語の違い、を言うならば、Molesの提供するものはモックではなくスタブ。自動生成クラスにつくプリフィックスのSは勿論Stubですが、MはMockではなく、Moleを指します(じゃあMoleって何よ、っていうと、何なんでしょうね……)

さて、使い分けとかいうほどのものでもないので、基本はMolesのみでいいんじゃないかなあー。もし呼び出しを保証したければ、こういうふうに書ける。

// IDisposableのDisposeは1回しか呼ばれないとしたい場合を検証する
// インターフェイスの場合はMHogeではなくSHogeなことに注意
var callCount = 0;
var mock = new SIDisposable()
{
    Dispose = () => { callCount += 1; }
};

(mock as IDisposable).Dispose(); // mockを使った処理があるとする...
callCount.Is(1); // 1回のみ

フレームワークに用意されていないから一手間なのは事実ですが、Molesの持つシンプルさを失ってまで足したいほどでもなく、好きなようなチェックを自前で書けるのだから、それでいいかな。むしろこのほうが大抵スッキリ。といったようなことは、MolesのマニュアルのComparison to Existing Frameworksに書かれています。Molesの提供するシンプルさが、私は好きです。

フレームワークは最大限のシンプルさを保って、機能は他の機構に回すというのはChaining Assertionも一緒ですよ←比較するとはなんておこがましい

もう一つ、もっと具体的なもので行きましょうか。LinqのCount()はICollectionの場合は全部列挙せず、Countプロパティのほうを使ってくれる(詳細は過去記事:LinqとCountの効率をどうぞ)ことのテスト。

var countCalled = false;
var enumeratorCalled = false;
var mock = new SICollection01<int>
{
    CountGet = () => { countCalled = true; return 100; },
    GetEnumerator = () => { enumeratorCalled = true; return null; }
};

// 呼んでるのはLinqのCount()のほうね
mock.Count().Is(100);
countCalled.Is(true);
enumeratorCalled.Is(false);

CountGetで100返せば、それだけでいい気もしますが、念のため+意図を表明するということで。

そういえばですが、Chaining AssertionのIsは、散々DisったAssertThatに存外近かったりします。 Assert.That(actual, Is(expected)) と書くものを、 actual.Is(expected) と書けるようになった、ですから(但しこれはAreEqualsの場合であって、Shuold.Be.GreaterThanとかやり始めたらぶん殴る)。

Silverlight? Windows Phone 7?

Silverlightのテスト環境は貧弱です。当然それに連なってWP7のテスト環境も貧弱です。というかMSTestが使えない!というだけじゃなく、Molesも動かせませんし。どうする?そこは、「リンクとして追加」でSilverlight/WP7のファイルをWPFのプロジェクトにでも移して、そのWPFのコードをテストするという手段が取れなくもないです。非同期周りはRxが吸収出来るし、互換性は、元来クラス群が貧弱なSLのほうが第一ターゲットなので、まあまあ大丈夫なはず。ViewModelはともかくとして、Modelのテストなら行けるはずです。

非同期のテストは難しいって?うん、Rxを使えば簡単なんだ。大丈夫。

まとめ

次世代というか、もう現世代なんですよ。今まで理想論に過ぎなかったものを、急速に現実のものとしてくれています。徒手空拳では難しい領域はいっぱいあった。でも、今、手元にはRxとMolesがある。この二つを手に、もう一度領域を見てみたらどうだろう?晴れた景色が広がっているはずです。

それにしてもRxの素晴らしさがMolesで更に輝くことといったらない。

今回はRxと組み合わせた例を中心に説明しましたが、Molesは単体でも文句なく素晴らしい。Moqも悪くないけれど、選ぶならMolesです。とにかく抜群に使いやすい。機能が極まっていることと、APIのシンプルさは両立するんだって。自動生成を活かしきった事例ですねー。VSとのシームレスな一体化といい、文句のつけようがない。ついこないだまで軽視していた私が言うのもアレですが、これがそんなに知られていない(少なくともググッて引っかかる記事はid:jamzzさんの記事だけだ)のは勿体無い話。Moles、是非試してみてください。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive