Microsoft Fakes Frameworkの使い方

Fakes FrameworkはVisual Studio 2012から搭載されたユニットテスト用のもっきゅっきゅライブラリです。いや、ライブラリというには大掛かりなので、やっぱFrameworkでしょうか。ともあれ、そんなもののようなものです。ドトネトだと競合で最も有名なのはMoqですね。競合との大きな違いは、通常のもっきゅっきゅライブラリがinterfaceやvirtualなメソッド類しか上書きできないのに対して、FakesはStaticメソッドやふつーの非virtualメソッドすらも上書き出来ちゃうところにあります。つまり、なんでもできます。

そして、Visual Studio Ultimateじゃないと使えません。……うぉーん。と、いうわけで、強力さはよーく分かるんですが、Ultimateでしか使えないところに萎えていたりしました。が、Visual Studioへの要望を出すForumでProvide Microsoft Fakes with all Visual Studio editionsといった投票が以前からあり(私もVote済みです)、そこでついに最近、全エディションに搭載するよう検討するから待っててね!というMSからの返答が!やったね!あ、まだVoteしてない人はVoteしましょう。

さて、Fakesは元々Molesという名前で、PexというMicrosoft Researchで開発されていた(今はメンテされてるのかなあ、怪しいなあ)自動テストツールの付属品みたいな感じで存在していました。できることは、既存のクラスの静的/インスタンスメソッドやプロパティの動作を、自由に置き換えることです。もうこれ本当に素晴らしくて、一度使うとMoles抜きのテストとか考えられないぐらいで、このサイトでもRx + MolesによるC#での次世代非同期モックテスト考察とかRxとパフォーマンスとユニットテストとMoles再びといった記事で紹介してきました。どちらもRxとセットで書いていますが、Moles自体は別にRx関係ありません。

ちなみに同様のことができるライブラリにはTypemock IsolatorJust Mockがありますが、何れも有償です(結構お高い、まぁVisual Studio Ultimateほどではないですが!)。Fakesとそれら(やMoq)の違いはもうひとつあって、Fakesは自動生成が基盤になっているので、メソッドやプロパティの置き換えが同様の定義をラムダ式で渡すだけという、非常にスムーズなやりかたで済みます。他のものは、基本的にはSetUp.Returnsとか、流れるようなインターフェースが基調になっていて、そんな書きやすいわけではないんですね。機能が強力だという他に、モック定義が超簡単、というのもFakesの大きな魅力です。

使い方

詳細な使い方とかガイドはIsolating Code under Test with Microsoft Fakesにありますが、まあ簡単に見てきましょうか。ユニットテストプロジェクトの参照設定でSystemを右クリックしてFakesアセンブリに追加をクリック。

するとFakesフォルダの下にmscorlib.fakesとSystem.fakesが作られます。そして、暫く待つとmscorlib.4.0.0.0.FakesとSystem.4.0.0.0.Fakesが追加されます。これ、バックグラウンドに必死に解析しているといった感じなので、割と待たされます(せめてステータスバーで通知してくれてれば分かりやすいのですが)。すぐにFakesが追加されなくてオカシイなー、とかドーナッテンダー、とか思うかもですが、まあゆるりと待ちましょう。待つといっても1分は待たないかな、さすがに、マシン性能にもよるでしょうが。

これでとりあえず準備完了。

一番単純かつよく使うかつ有意義かつそこらじゅーで紹介されている例としては、DateTime.Nowの差し替えなので、まずそれを見ますか、定番お馴染みですけれど。Assertには別ライブラリのChaining Assertionを使います。Assert.AreEqual(25, Math.Pow(5, 2))がMath.Pow(5, 2).Is(25)といったようにメソッドチェーンでサクッと書けて可読性良くて実にいい(宣伝)。

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        // Shim"s"Contextで囲むとその中でShim使える(Stubだけ利用なら不要)
        using (ShimsContext.Create())
        {
            // DateTime.Nowを1999年12月31日に差し替え!
            ShimDateTime.NowGet = () => new DateTime(1999, 12, 31);

            // なのでDateTime.Nowは1999年です!
            DateTime.Now.Year.Is(1999);
        }
    }
}

どーでもいーんですがShimContextと間違えて、でてこないなあ、と悩んだりはよくしてました。正しくはShimsContextですねん。ともあれ、超簡単に難問であるDateTimeの差し替えに成功しました!素晴らしい!

さて、もっきゅっきゅライブラリによくある機能はもう一つ、差し替えたメソッドが呼ばれたかどうかの検証があります。これに関してはFakesは特にライブラリ側でサポートはしていません。自前でやります。例えば……

var calledCount = 0;
var stub = new StubIEnumerable<int>
{
    GetEnumerator = () => { calledCount++; return Enumerable.Range(1, 10).GetEnumerator(); }
};

stub.Count().Is(10); // LINQのCountを使ってGetEnumeratorを呼んだ

calledCount.Is(1); // 1回呼ばれた、という検証

StubはふつーのMoqライブラリで定義可能なのと同じで、interfaceかvirtualなメソッドを置き換えられます。ラムダ式で定義出来るのが、やっぱ簡単でイイですね。で、検証のやり方は、単純に外部に変数定義してそれ呼んでやって、という地味ーで原始的な手法が正解。手間といえば手間ですが、Moq定義がシンプルなので、違和感は全然ないです。

Verify用拡張の実装

とはいえ、定形パターンでラムダの外に変数置いてどうこう、というのも面倒くさいので、Verify用にちょっと作ってみました。例えばこんな風に使います。

[TestMethod]
public void IListUseCountByLINQ()
{
    var enumerator = Verifier.Zero("IList.GetEnumerator"); // 文字列入れておくとエラー時にどの検証で失敗したのか判別できる。
    var count = Verifier.Once(); // 省略も可能だけどエラー時に不明になるので、メソッド内で検証は一個ののみとか限定で。

    IEnumerable<int> list = new StubIList<int>()
    {
        // 各メソッド先頭でCalledを呼ぶと内部のカウンターがIncrementされる
        GetEnumerator = () => { enumerator.Called(); return Enumerable.Empty<int>().GetEnumerator(); },
        CountGet = () => { count.Called(); return 10; },
    };

    list.Count(); // LINQのCount()メソッドを使う

    Verifier.VerifyAll(count, enumerator); // Countは一度呼ばれてGetEnumeratorは一度も呼ばれてないことの検証実行
}

もしこれでvar enumerator = Verifier.Once("IList.GetEnumerator") にすると、VerifyAllのところで「System.Exception: Verify Error - Key:IList.GetEnumerator, Condition:(x == 1), CalledCount:0」という例外が発生して、実行されたかの検証が行える、みたいな感じですん。エラーメッセージもそこそこ親切。

ちょっと面倒かなあ、いちいち変数定義するのダルいなあ、とかとも思いますが、まあ何もないよりは良いのではないでしょうか。以下はその実装。

public class Verifier
{
    public static Verifier Zero(string key = "")
    {
        return new Verifier(key, x => x == 0);
    }

    public static Verifier Once(string key = "")
    {
        return new Verifier(key, x => x == 1);
    }

    public static Verifier Create(Expression<Func<int, bool>> condition)
    {
        return new Verifier("", condition);
    }

    public static Verifier Create(string key, Expression<Func<int, bool>> condition)
    {
        return new Verifier(key, condition);
    }

    public static void VerifyAll(params Verifier[] verifier)
    {
        foreach (var item in verifier)
        {
            item.Verify();
        }
    }

    readonly Expression<Func<int, bool>> condition;
    int count;
    public int Count { get { return count; } }
    public string Key { get; private set; }

    private Verifier(string key, Expression<Func<int, bool>> condition)
    {
        this.Key = key;
        this.count = 0;
        this.condition = condition;
    }

    public void Called()
    {
        Interlocked.Increment(ref count);
    }

    public void Verify()
    {
        if (!condition.Compile().Invoke(count))
        {
            var msg = string.Format("Key:{0}, Condition:{1}, CalledCount:{2}", Key, condition.Body, count);
            throw new Exception("Verify Error - " + msg); // 例外は最終的に独自例外を使う
        }
    }
}

複数回呼ばれることの検証はVerifier.Create(x => x == 10)とかVerifier.Create(x => x >= 1)とかって書きます。ここをTimes.ExactlyだのTimes.AtMostOnceだのTimes.Betweenだのとメソッド名でやりくりさせる流れるようなインターフェース(笑)的なやり方は嫌いですねえ(Timesは別に流れてませんが)。ラムダ式あるんだからそれ使うべきでしょ常識的に考えて。

これはただのコンセプトですが、もう少し練りこんだらChaining Assertionに入れましょう。

WebRequestのShimを作りたい場合

ところで、mscorlibとSystemのFakeが標準で作られるわけですが、それの中身、少ないですよね?WebClientはないし、WebRequestもStubばかりでShimがないし。どうなってるの?

mscorlibとSystemは巨大なライブラリなため、全てのFakeを作っていると量が膨大すぎて処理に時間がかかります。だから、デフォルトでは生成されるものが限定的になっています。じゃあどうすればいいのか、というと、.fakesの中身(XML)を編集して、明示的に生成するものを指定してあげれば解決します。

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
    <Assembly Name="System" Version="4.0.0.0"/>
    <ShimGeneration>
        <Add FullName="System.Net.HttpWebRequest" />
    </ShimGeneration>
</Fakes>

Visual Studioで編集すればIntelliSenseが効くので、迷いなくできるでしょう。StubGenerationに対するオプションがあったり、Disableのtrue/falseが指定できたりとか、IntelliSenseに従うだけで発見できます。書き換えたらビルドすれば、設定の反映されたDLLに置き換えられます。もし置き換わらなかったら、テストプロジェクトのFakesAssembliesフォルダの中身を全部消して再ビルドしてみましょう。それでも追加されていなかったら、.fakesの書き換えミスでしょうね。私はFullNameとTypeNameを間違って追加されねー、と悩んだりしたことあります。

さて、じゃあ実際に↑のHttpWebRequestへのShimを使って、例えばHttpClientは最終的にWebRequestで実行されてるんだー、というのを検証するには……

// 非同期メソッドをテスト対象にする時はTaskを戻り値にする
[TestMethod]
public async Task HttpClientIsWrapperOfHttpWebRequest()
{
    using (ShimsContext.Create())
    {
        var v = Verifier.Once();

        // どこかで生成される全てのInstanceを対象にするには.AllInstances経由で
        // 第一引数はそのインスタンスそのものがくる
        ShimHttpWebRequest.AllInstances.BeginGetResponseAsyncCallbackObject = (instance, callback, state) =>
        {
            v.Called();
            // ExecuteWithoutShimsで差し替えていないオリジナルのものを呼べる
            return ShimsContext.ExecuteWithoutShims(() => instance.BeginGetResponse(callback, state));
        };

        await new HttpClient().GetAsync("http://google.co.jp/");

        v.Verify();
    }
}

といったように書けました。ExecuteWithoutShimsとか、色々配慮されてて良い感じですねー。

ところでWebRequestは、IWebRequestCreateのStubを作ってWebRequest.RegisterPrefixにそれを登録するとWebRequest.Createは乗っ取ることが可能です、実は何気に。

var webreq = new StubIWebRequestCreate { CreateUri = uri => { /* hogemoge */ } };
WebRequest.RegisterPrefix("http://", webreq);

そして、これで実際WebClientのDownloadStringとかのWebRequest生成はフックできます。でも、これだと.NET 4.0から追加されたWebRequest.CreateHttpは乗っ取れないし、HttpClientにいたってはinternalなコンストラクタを使ってnew HttpWebRequestしているので、もはやそんな手法は実質完全無意味だ!ほんと、このあたりグダグダなので何も考えないほうがいいです。色々と幻想すぎる。

Shim vs Stub

vsというか、まずInterfaceはStubしか作れません。ある意味当たり前ですね。具象型は、Shimで作れば何でも差し替えられる、Stubで作るとvirtualなもののみ差し替えられる。具象型に関してはShimはStubの完全なる上位互換です。じゃあStub要らないのか、というと、割とそうでもなくて、Stubは軽量です。Shimは書き換えが入るので重たいです。このことはアプリケーションの設計全体に通しても言えて、Shimで何でも差し替えられるから、全面的にShimに頼ろう!みたいなのはダウトです。ダメ。それなりにテスタビリティを考慮した設計(= Stubで差し替え可能な状態)を作ったほうが良いです。

ただ、理想的な形がShimがゼロな状態でもテスタビリティ100%にすること、だとは私は思ってません。テスト可能にするために、ある程度、素直な設計を犠牲にして、歪んだ形になることって往々にあるはずです。そういうところは素直にShim使ったほうが100億倍良いでしょう。まあ、そのバランスに関しては答えなんてないので、各自で適宜、線を引いていくしかないかなーって思ってます。

あ、どうでもいいんですが、私はリポジトリパターンって嫌いで、いや、リポジトリパターンというか、ほぼほぼ100%テストのためだけにIHogeRepositryとHogeRepositryという実態作るとかIHogeとHogeImplが必ずといっていいほどセットなJxxxみたいじゃんというか、本当に嫌ですね!大嫌いですね!じゃあどうするかっつったら割とどうにもならないところもあるし、それをShimでサクッと殺すのがいいとは全然思いませんが、しかし私はShimで殺すことを選びますね。

まとめ

Fakes Frameworkは半端無く強力なので、とっとと全エディションに搭載されるといいなあ。Visual Studio 2012 SP1(いつ?)とかで、ね。いや、それじゃ遅すぎる、もっと早く!もっと早くに!Molesの頃はちょっと挙動に不安定さを感じた時もありましたが、さすがにプロダクト正式搭載なFakesは安定感もあってすっごくイイ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive