Archive - 2012.11

Chaining Assertion 1.7.0.1 - 値比較の追加

EqualsやGetHashCodeをオーバーライドするかと言ったら、そういう目的があるなら当然するし、そうでないならしない。という極当たり前なところに落ち着いたりはする。目的ってどういう時かといったら、LINQでDistinctの対象にしたい時とかですかね、まあ、よーするに値で比較したい!時です、まんまですね。

なので、逆にそれ以外の用途であえてこれらをオーバーライドすることはないです。特にテストのためにオーバーライドというのは、はっきしいって、良くないって思ってます。Equalsというのはクラスとして非常に重要な意味のあるところなので、そこにテスト都合が混じりこむのはNGです。

でも、テスト都合で値で比較したかったりは割とあるんですよね。じゃあどうするかって、まあ、ふつーにアサート側で構造比較したほうがいいでしょ、テスト都合なんだから。QUnitなんてdeepEqualが基本で、それが存外使いイイんですよ。

と、いうよくわからない前振りですが、つまるところChaining Assertionに値で比較できるIsStructuralEqualを足しました。

いやあ、一年ぶりの更新です!というか、もう一年前ですかー、早いものだ。もうAssert.ThatをDisるのも忘れてたぐらいに昔の話ですねー、あ、今も当然Assert.ThatとかFluent何ちゃらは嫌いですよ、と、それはさておき。かなり前の話なのでChaining Assertionについておさらい。メソッドチェーンな感じにさらさらっとAssertを書けるテスト補助ライブラリです。詳しくはメソッドチェーン形式のテスト記述ライブラリという1.0出した時の説明をどうぞー。主にMSTestやNUnitに対応しています。勿論NuGetでも入りますのでChainingAssertionで検索を。

今回追加したのはIsStructuralEqual(もしくはIsNotStructuralEqual)で、構造を再帰的に辿って値としての一致で比較します。

// こんなクラスがあるとして
class MyClass
{
    public int IntProperty { get; set; }
    public string StrField;
    public int[] IntArray { get; set; }
    public SubMyClass Sub { get; set; }
}
 
class SubMyClass
{
    public DateTime Date { get; set; }
}
 
 
[TestMethod]
public void TestMethod1()
{
    var mc1 = new MyClass
    {
        IntProperty = 100,
        StrField = "hoge",
        IntArray = new[] { 1, 2, 3, 4, 5 },
        Sub = new SubMyClass
        {
            Date = new DateTime(1999, 12, 31)
        }
    };
 
    var mc2 = new MyClass
    {
        IntProperty = 100,
        StrField = "hoge",
        IntArray = new[] { 1, 2, 3, 4, 5 },
        Sub = new SubMyClass
        {
            Date = new DateTime(1999, 12, 31)
        }
    };
 
    mc1.IsNot(mc2); // mc1とmc2は全て同じ値ですが、参照比較では当然違います
 
    mc1.IsStructuralEqual(mc2); // IsStructuralEqualでは全てのプロパティを再帰的に辿って比較します
}

なんのかんので便利で使っちゃいますね、多用しちゃいますね、きっと。ちなみに、ただたんに値比較したいだけなら、JSONにでもDumpして、文字列一致取ればいいだけなんですけれど。自前で辿ることによって、誤った箇所へのメッセージは割と親切かな、と思います。あとIEquatableの扱いとか型の扱いとか、ただのDumpとは色々若干と違うので、まあ、こちらのほうが望ましい具合な結果が得られると思います。

var mc1 = new MyClass
{
    IntProperty = 100,
    StrField = "hoge",
    IntArray = new[] { 1, 2, 3, 4, 5 },
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};
 
// 間違い探し!
var mc2 = new MyClass
{
    IntProperty = 100,
    StrField = "hoge",
    IntArray = new[] { 1, 2, 3, 100, 5 }, // 4番目が違う
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};
 
// 以下のようなエラーメッセージが出ます
// is not structural equal, failed at MyClass.IntArray.[3], actual = 4 expected = 5
mc1.IsStructuralEqual(mc2);
 
// こんどはStrFieldが違う
var mc3 = new MyClass
{
    IntProperty = 100,
    StrField = "hage",
    IntArray = new[] { 1, 2, 3, 4, 5 },
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};
 
// 以下のようなエラーメッセージが出ます
// is not structural equal, failed at MyClass.StrField, actual = hoge expected = hage
mc1.IsStructuralEqual(mc3);

割と十分、分かりやすい。かな?

コードは結構好き勝手感です。PropertyInfoとFieldInfoは共にMemberInfoを継承してるが、GetValueは同じメソッドシグネチャだけど、MemberInfoに定義されてるわけじゃなくてPropertyInfo, FieldInfoにそれぞれあるから共通でまとめられないよー→dynamicで受ければGetValue使えて大解決。とか、まあふつーのコードではやらないようなことも、UnitTest用だから若干の効率低下は無視でなんでもありで行くよ!というのが割と楽しいですね。

その他

ついでにjsakamotoさんからPull Request来ていたIsInstanceOfでメソッドチェーンできるようになったり(Pull Requestの放置に定評のある私です!というか初めてacceptしたわー)、IsTrueとIsFalseを足した(いやあ、Is(true)ってやっぱ面倒くさかったよ、あはは)りなどしました。

さて、お次はWinRT対応とWindows Phone 8対応を、といったところなのですが、それはそのうち近いうちに!WinRT対応はねえ、リフレクション回りがドサッと変わってるので面倒といえば面倒なんですよねえ。まあ、勉強のためのちょうどいい題材ではあるので、手を付けたいとは思ってるのですけれど。

あとね、正直NUnitはいいとしてもMbUnitやxUnit、SLやWP7に対応させるの超面倒くさい。やりすぎた。これのせいでちょっとした修正ですら大仕事なわけですよ。このメンテコスト最悪すぎる状態がどうにもねえ、それでいてNUnitはともかく、その他なんてほとんど使われてないですからねえ。分かってはいたのですが、こうシンドイと結構限界。というわけで、WP7とSilverlightは削除しました。この二つはもういらないぢゃん?さようなら……。

ああ、あとFakes FrameworkのためのVerifierも入れたいしねえ、やりたいことは割と多いんですが、ニートもこれはこれでいて忙しくて手が回らないのですよー。

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は安定感もあってすっごくイイ。

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Visual Studio and Development Technologies(C#)

April 2011
|
July 2018

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc