Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)

C#と親和性の高いデータ形式はXMLです。何と言ってもLinq to Xmlが強力です。また、SOAPも悪くない、というのもVisual Studioの自動生成が効くので何も考えずともホイホイ使えます。ではJSONは、というと、これは割と扱いづらいところがあるのが正直なところ。しかしWindows Phone 7においては、JSONを選択すべきでしょう。なにせ、モバイル機器。ネットワークがとても貧弱。データは小さいに越したことはない。XMLとJSONとでは、雲泥の差です。

WPFではJsonReaderWriterFactory(と、内部にそれを用いたDynamicJson)、SilverlightではSystem.Jsonなどが用意されていますが、WP7には一切ありません。じゃあどうするかといえば、シリアライザを使います。WP7ではDataContractJsonSerializerが標準で用意されている(WPF, SLにもあります)ので、それを使ってデシリアライズしてJSONをオブジェクトに変換するのが基本戦略となります。

外部ライブラリ、Json.NETを使うという手も勿論ありますが。

BingからのJSONの取得

何はともあれ、サンプル題材のJSONを拾ってきましょう。Webからの取得というと、最近はいつもTwitterのPublic Timelineでマンネリ飽き飽きなので、別のものを。WP7なので、Bing APIを使いましょう!Bing APIはIDを取得しないと使えないのでサンプル的にどうよ、というところもありますが、IDの取得は簡単(ほんとワンクリックです)だしWP7と親和性の高いAPIでもあるので、これを機に、試しに取ってみるのも良いのではと思います。画像検索、翻訳など色々種類があるのですが、今回はWeb検索(sources=web)にします。

// 標準WP7テンプレのMainPage.xaml.csにベタ書き

const string AppId = ""; // AppIdは登録してください

Uri CreateQuery(params string[] words)
{
    // countなどは変数で置き換えれるようにするといいのではと思います、ここでは固定決め打ちですが
    var query =
          "?Appid=" + AppId
        + "&query=" + Uri.EscapeUriString(string.Join(" ", words))
        + "&sources=web"
        + "&version=2.0"
        + "&Market=ja-jp"
        + "&web.count=20"
        + "&web.offset=0";

    return new Uri("http://api.search.live.net/json.aspx" + query);
}

public MainPage()
{
    InitializeComponent();

    var wc = new WebClient();

    Observable.FromEvent<DownloadStringCompletedEventHandler, DownloadStringCompletedEventArgs>(
            h => h.Invoke, h => wc.DownloadStringCompleted += h, h => wc.DownloadStringCompleted -= h)
        .ObserveOnDispatcher()
        .Subscribe(e =>
        {
            var json = e.EventArgs.Result; // ダウンロード結果(json文字列)
            MessageBox.Show(json);
        });

    wc.DownloadStringAsync(CreateQuery("地震"));
}

json.aspxにクエリ文字列をつけてGETするだけなので割とお手軽。クエリ文字列がゴチャゴチャして分かりづらいのですが、基本的に弄るのはqueryとweb.countぐらいかな、と思います。BingのReferenceは、生成元のクラス構造がまんま掲示されているだけで、恐ろしく分かりづらいので、適当にサンプルから当たりをつける感じで。

非同期通信の実行はReactive Extensions(Rx)で行います。Windows Phone 7では標準で入っているのでSystem.ObservableとMicrosoft.Phone.Reactiveを参照に加えてください。非同期通信を生でやるなんてありえませんから!Rx利用を推奨します。

得られるJSONは下記のものです。

{
    "SearchResponse": {
        "Version": "2.0",
        "Query": {
            "SearchTerms": "地震"
        },
        "Web": {
            "Total": 88,
            "Offset": 0,
            "Results": [
                {
                    "Title": "地震情報 - Yahoo!天気情報",
                    "Description": "Yahoo!天気情報は、市区町村の天気予報、世界の天気...",
                    "Url": "http://typhoon.yahoo.co.jp/weather/jp/earthquake/",
                    "DisplayUrl": "typhoon.yahoo.co.jp/weather/jp/earthquake",
                    "DateTime": "2011-03-29T19:11:00Z"
                },
                {
                    "Title": "地震情報 :: ウェザーニュース",
                    "Description": "最新の地震の震度、震源地、震度分布を速報で届けます...",
                    "Url": "http://weathernews.jp/quake/",
                    "DisplayUrl": "weathernews.jp/quake",
                    "DateTime": "2011-03-28T09:10:00Z"
                },
                // 配列上なので幾つも...
            ]
        }
    }
}        

JSONに関してはJSON ViewerをVisual StudioのVisualizerに組み込むとかなり快適にプレビュー出来るようになります。が、カスタムVisualizerはWP7では実行出来ないのでテキストで見て、スタンドアロンのものにコピペってのを実行ですね、しょんぼり。

DataContractJsonSerializer

では、JSONをオブジェクトに変換しましょう。基本的には、1:1に対応するクラスを作るだけ。必要に応じて System.Runtime.Serializationの参照を加えDataContract, DataMember属性なども加えればよし。

public class BingWebRoot
{
    public SearchResponse SearchResponse { get; set; }
}

public class SearchResponse
{
    public string Version { get; set; }
    public Query Query { get; set; }
    public Web Web { get; set; }
}

public class Query
{
    public string SearchTerms { get; set; }
}

public class Web
{
    public int Total { get; set; }
    public int Offset { get; set; }
    public Results[] Results { get; set; }
}

public class Results
{
    public string Title { get; set; }
    public string Description { get; set; }
    public string Url { get; set; }
    public string DisplayUrl { get; set; }
    public string DateTime { get; set; }
}

JSONは、JavaScriptのオブジェクトとほぼ同一の記述ですが、ようするに{}になっている部分はクラスで、[]になっている部分は配列で、置き換えていけばいい、ということで。難しくはないのですが、面倒くさいには大変面倒くさい。なお、JSONの構造を全部記述する必要はなく、必要なものだけでも構いません、例えばVersionやQueryはいらないから省くとか、全然アリです。

そして、 System.ServiceModel.Web を参照設定に加え、DataContractJsonSerializerを使います。

var wc = new WebClient();

Observable.FromEvent<OpenReadCompletedEventHandler, OpenReadCompletedEventArgs>(
        h => h.Invoke, h => wc.OpenReadCompleted += h, h => wc.OpenReadCompleted -= h)
    .ObserveOnDispatcher()
    .Subscribe(e =>
    {
        using (var stream = e.EventArgs.Result)
        {
            var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
            var result = (BingWebRoot)serializer.ReadObject(stream);

            MessageBox.Show(result.SearchResponse.Web.Results[0].Title);
        }
    });

wc.OpenReadAsync(CreateQuery("地震"));

デシリアライズはReadObject、シリアライズはWriteObjectで行います。基本はstreamを渡すだけでオブジェクトの出来上がり。

with Reactive Extensions

ですが、まあ、Resultsが欲しいだけなのにSearchResponse.Web.Resultsは長げーよ、とか、DateTimeがstringでイヤだー、とか色々あります。そういう場合はJSONとのマッピング用のクラスとは別に、アプリケーション側で使うクラスを別に立ててやればいいんぢゃないかしら。

public class SearchResults
{
    public string Title { get; set; }
    public string Url { get; set; }
    public DateTime DateTime { get; set; }

    public override string ToString()
    {
        return DateTime + " : " + Title + " : " + Url;
    }
}

TitleとUrlとDateTimeしかいらない!という具合で。これを、今度はWebRequestを使って書くと

var req = WebRequest.Create(CreateQuery("ほむほむ"));

Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
    .Select(r =>
    {
        using (var stream = r.GetResponseStream())
        {
            var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
            return (BingWebRoot)serializer.ReadObject(stream);
        }
    })
    .SelectMany(x => x.SearchResponse.Web.Results)
    .Select(x => new SearchResults { DateTime = DateTime.Parse(x.DateTime), Title = x.Title, Url = x.Url })
    .ObserveOnDispatcher()
    .Subscribe(x => 
    {
        // 加工は全部終わってるのでここで色々自由に処理
        Debug.WriteLine(x);
    });

となります。最初のSelectは非同期の結果、次のSelectManyではResults[]、つまり普通の配列を平坦化して、以降は普通のLinqのようなコレクション処理をしています。

非同期リクエストとオブジェクトのコレクション処理が、完全にシームレスに溶け込んでいます。これが、RxがLinqとして存ることの真価の一つです。記述が統一され、かつ限りなくシンプルになる。Rxは非同期が、イベントが、時間が、簡単に扱えます。でも、本当の真価は単独で使うというだけでなく、それらが全てPush型シーケンスに乗っていることで、統合することが可能だというところにあります。

でも、むしろ分かりにくい?ふむむ……。慣れの問題、などというと全く説得力がなくてアレですが、しかし、慣れです。記述がシンプルになり、柔軟性と再利用性が増していることには間違いないわけで、後は一度全て忘れてLINQの世界に飛び込んでしまえばいいと思うんだ。

Linqは各処理の単位が細分化されている(Selectは射影、Whereはフィルタ)ことも特徴ですが、これは思考の再利用可能性を促します。非同期->オブジェクト配列=SelectManyなど、単純な定型パターンに落とし込めます。C#はもとより強力なIntelliSenseにより、ブロックを組み立てるかの如きなプログラミングを可能にしていますが、Linqでは、それが更に先鋭化されていると見れます。

まとめ

これも現在製作中のWP7アプリからの一部です。最近Bing API利用に切り替えたので。無駄に汎用化して作りこみつつきりがないので適度なところできりあげつつ。ユニットテスト作ってあったので移行自体は幸いすんなりいった。良かった良かった。テスト大事。

Bing APIの前は諸事情あってGoogleからのスクレイピングでした。スクレイピングはグレーだろうということで代替案をずっと探していて、何とかBingに落ち着きました。最初はどうにも使い物にならない、と思ったのですが、検索パラメータを色々変えて、ある程度望む結果が出るようにはなったかな、と。Bingは結構癖があって、調整大変ですね。その話は後日、WP7アプリが完成したときにでも……。

コード的にはスクレイピングのほうも割と凝ってたんですけどねー、バッサリとゴミ箱行き。復活することは、ないかな。もったいないけどしょうがない。いつかそのうち紹介する日は、来るかも来ないかも。

そんなわけで延々と足踏みしていて実装は相変わらず一歩も進んでませんが(!) 順調に制作は進行中なので乞うご期待。いやほんと。

Widows Phone 7でアプリケーション名やバージョン番号をバインドする方法

アプリケーション名の表示や、about画面に表示したいであろうバージョン番号、どうします?直書きのstringやリソースから?それもいいのですけれど、せっかくプロジェクトのプロパティ(AssemblyInfo.cs)で、アプリケーション名やバージョン番号を設定しているわけだから、そこから利用できたほうがいいですよね。

というわけで、これらの情報はアセンブリから取得しましょう。

public class AssemblyInfoData
{
    public static readonly AssemblyInfoData ExecutingAssembly = new AssemblyInfoData(Assembly.GetExecutingAssembly());

    public string FileName { get; private set; }
    public string Version { get; private set; }
    public string FileVersion { get; private set; }
    public string Title { get; private set; }
    public string Description { get; private set; }
    public string Configuration { get; private set; }
    public string Company { get; private set; }
    public string Product { get; private set; }
    public string Copyright { get; private set; }
    public string Trademark { get; private set; }
    public string Culture { get; private set; }

    public AssemblyInfoData(Assembly assembly)
    {
        var assemblyName = new AssemblyName(assembly.FullName);
        FileName = assemblyName.Name;
        Version = assemblyName.Version.ToString();

        FileVersion = GetAttributeName<AssemblyFileVersionAttribute>(assembly, a => a.Version);
        Title = GetAttributeName<AssemblyTitleAttribute>(assembly, a => a.Title);
        Description = GetAttributeName<AssemblyDescriptionAttribute>(assembly, a => a.Description);
        Configuration = GetAttributeName<AssemblyConfigurationAttribute>(assembly, a => a.Configuration);
        Company = GetAttributeName<AssemblyCompanyAttribute>(assembly, a => a.Company);
        Product = GetAttributeName<AssemblyProductAttribute>(assembly, a => a.Product);
        Copyright = GetAttributeName<AssemblyCopyrightAttribute>(assembly, a => a.Copyright);
        Trademark = GetAttributeName<AssemblyTrademarkAttribute>(assembly, a => a.Trademark);
        Culture = GetAttributeName<AssemblyCultureAttribute>(assembly, a => a.Culture);
    }

    private string GetAttributeName<T>(Assembly assembly, Func<T, string> selector) where T : Attribute
    {
        var attr = assembly.GetCustomAttributes(typeof(T), true).Cast<T>().FirstOrDefault();
        return (attr == null) ? "" : selector(attr);
    }
}

FileName, Versionはnew AssemblyNameに渡してから(WPFだとGetNameで直に取れるのですが、Silverlightだとセキュリティ違反で例外が飛ぶためこうする必要がある)、それ以外の値はカスタム属性から取得できます。また、任意のAssemblyの情報をコンストラクタに投げて取得出来るようになっていますが、どうせ必要なのは実行アセンブリの情報だけでしょ?ってことで、public staticなフィールドにExecutingAssemblyのデータを公開するようにしています。なので、コードからは、 AssemblyInfoData.ExecutingAssembly.Version とアクセスするだけで、簡単に取得できます。

でも、コードから欲しいということはほとんどなくて、UIに表示するためだけに欲しいのですよね、こういう情報は。バインディングしましょう!まず、こんなクラスを用意します。

public class AssemblyInfoDataBindingHelper
{
    public AssemblyInfoData Value { get { return AssemblyInfoData.ExecutingAssembly; } }
}

何故これが必要かというと、WPFの場合は{x:static}でstatic変数もバインド出来るのですが、Silverlight/WP7ではバインド出来ないためです。いやあ、カッコ悪いですね、{x:static}欲しいですね、まあ、ないものはしょうがない。

次にApp.xamlのApplication.Resourcesの中に

<Application.Resources>
    <!-- Applicationのところで xmlns:local="ネームスペース" を宣言しておく-->
    <local:AssemblyInfoDataBindingHelper x:Key="AssemblyInfoData"/>
</Application.Resources>

と書いてリソースを登録。準備はこれで完了で、あとはバインドするだけ。

<TextBlock x:Name="ApplicationTitle" Text="{Binding Value.Title, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="{Binding Value.Version, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextTitle1Style}" />

と、以上です。これで下のような感じに

表示されました。ApplicationTitleにTitleは分かりますがPageTitleにVersionは丸っきりイミフ。ボタンは、何となく寂しいから置いただけで意味はないです気にしないで。

AssemblyInfoDataクラスのコードは完全に独立して使い回しが効くので、コピペってどうぞご自由にお使いください。煮るなり焼くなり……、パブリックドメインで。

まとめ

といったのは、一応、今製作中のWP7アプリの一部です。順調に制作は遅れまくり。うむむ。今月中といきたかったのだけど、まーだずれ込みそう。その前は二月中のつもりだったのだけど、Chaining Assertionが思いの外引っ張りすぎて手を付けてる余裕がなかった。とにかく、4月中頃までには、マーケットプレイスで公開したいなあ。あとソースコードも公開します(というか既に製作中のがこっそり公開されてます)。Reactive Extensionsの実践例として、ただたんに非同期で使うというだけじゃなく、こういうケースで使える、コードはこうなる。というサンプルとして役立てればいいな、という思いで書いてますので、適当に待っていてください。

人に見せるためのコード、というのを念頭に置きすぎていて、同じ場所のコードの修正ばかり繰り返していてアプリ全体としては一歩も製作が進まないという超鈍足状態に陥ってますが(コード書きの遅さに定評のある私です(キリッ)、でも、書きなおす度に確実によくなっていく実感はあるので、最終的にそこそこ見せれるコードになるのではないかと思っています。少なくとも、部分的には面白い内容になるはずです。

DynamicAccessor - Chaining Assertion ver.1.4.0.0

テストブームはまだ続いています。さて、テスト可能性の高い設計は良いのですが、本来あるべきである設計を歪めて(単純なところでいえば、virtualである必要でないものをvirtualにするとか、privateであるべきものをprotectedにするとか、無駄なinterfaceとか、不自然な引数の取り方とか)テスト可能性を確保するのは、私は嫌だなー。などと思っていましたが、しかし、モックについてはMolesを使うことで、最高の形で解決しました。

次に何を考えるべきかな、と浮かんだのはprivateのテスト。privateのテストは考えないという流儀もあるし、それは尤もだと思いますが、publicのものをテストするにも、ちょっと確認とりたかったり値を弄ってやりたかったりなど、触れると楽な場合もいっぱいあるので、出来るにこしたことはありません。MSTestにはAccessorの自動生成で完全なタイプセーフとIntelliSenseの保証をしてくれて、それはそれで大変素敵。なのですが、もう少し軽くテスト出来る機構を用意しました。MSTestへの依存はないので、NUnitでも他のテストフレームワークでも使えます。

privateへのアクセスはリフレクションが常套手段ですが、C#4.0ならdynamicがあるよね。というわけで、dynamicで包んだアクセサを用意しました。dynamicにしたからってprivateのものは呼び出せないので、DynamicObjectで包んでリフレクション経由になるようにしています。

AsDynamic()

こんな感じで使えます。

// こんなprivateばっかなクラスがあったとして
public class PrivateMock
{
    private string privateField = "homu";

    private string PrivateProperty
    {
        get { return privateField + privateField; }
        set { privateField = value; }
    }

    private string PrivateMethod(int count)
    {
        return string.Join("", Enumerable.Repeat(privateField, count));
    }
}

// AsDynamic()をつけるだけでPrivateプロパティが呼べる
var actual = new PrivateMock().AsDynamic().PrivateProperty;
Assert.AreEqual("homuhomu", actual);

// dynamicは拡張メソッドが呼べないのでIsを使う場合はキャストしてくださいな。
(new PrivateMock().AsDynamic().PrivateMethod(3) as string).Is("homuhomuhomu");

// 勿論setも出来ます(インデクサもいけます。ジェネリックメソッドも若干の制限付きですが呼べます)
var mock = new PrivateMock().AsDynamic();
mock.PrivateProperty = "mogumogu";
(mock.privateField as string).Is("mogumogu");

オブジェクトへの拡張メソッドにより、全てのオブジェクトに対しAsDynamic()が使える状態です。IntelliSense汚染なので通常だとあまり許容できることではないのですが、UnitTestなのでOKだろう、と。AsDynamic()後はDynamicObjectとして、全ての呼び出しがリフレクション経由となり、public/privateのメソッド/プロパティ/フィールド/インデクサに自由にアクセス可能となっています。見た目は普通と全く一緒で大変自然なのがdynamicの利点。

dynamicの状態では拡張メソッドの呼び出しは不可能なので、IsによるAssertionを行う場合は、キャストして型を適用してやる必要があります。メンドクセーという場合はAssert.AreEqualなど、本来用意されているものはobjectが対象なので、そのまんま使えます。どちらでも好き好きでどうぞ。

ダイナミックとジェネリックとメソッド呼び出し

実装内部の話。DynamicObjectでTryInvokeMemberです。んで、最初はすんごく簡単に実装出来ると思ったんですよ!dynamicでリフレクション包むだけね、はいはい、余裕余裕、と。が、実際に書きだすとどうも引っかかる。オーバーロードが。ジェネリックが。型推論が。ふつーに呼んでるとうまくオーバーロードを解決してくれなくて、AmbiguousMatchException(あいまいな一致)を投げてくれます。なので、手動でマッチさせる必要があります。

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
    var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
    if (csharpBinder == null) throw new ArgumentException("is not generic csharp code");

    var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>).ToArray();
    var method = MatchMethod(binder.Name, args, typeArgs);
    result = method.Invoke(target, args);

    return true;
}

private Type AssignableBoundType(Type left, Type right)
{
    return (left == null || right == null) ? null
        : left.IsAssignableFrom(right) ? left
        : right.IsAssignableFrom(left) ? right
        : null;
}

private MethodInfo MatchMethod(string methodName, object[] args, Type[] typeArgs)
{
    // name match
    var nameMatched = typeof(T).GetMethods(TransparentFlags)
        .Where(mi => mi.Name == methodName)
        .ToArray();
    if (!nameMatched.Any()) throw new ArgumentException(string.Format("\"{0}\" not found : Type <{1}>", methodName, typeof(T).Name));

    // type inference
    var typedMethods = nameMatched
        .Select(mi =>
        {
            var genericArguments = mi.GetGenericArguments();

            if (!typeArgs.Any() && !genericArguments.Any()) // non generic method
            {
                return new
                {
                    MethodInfo = mi,
                    TypeParameters = default(Dictionary<Type, Type>)
                };
            }
            else if (!typeArgs.Any())
            {
                var parameterGenericTypes = mi.GetParameters()
                    .Select(pi => pi.ParameterType)
                    .Zip(args.Select(o => o.GetType()), Tuple.Create)
                    .GroupBy(a => a.Item1, a => a.Item2)
                    .Where(g => g.Key.IsGenericParameter)
                    .Select(g => new { g.Key, Type = g.Aggregate(AssignableBoundType) })
                    .Where(a => a.Type != null);

                var typeParams = genericArguments
                    .GroupJoin(parameterGenericTypes, x => x, x => x.Key, (_, Args) => Args)
                    .ToArray();
                if (!typeParams.All(xs => xs.Any())) return null; // types short

                return new
                {
                    MethodInfo = mi,
                    TypeParameters = typeParams
                        .Select(xs => xs.First())
                        .ToDictionary(a => a.Key, a => a.Type)
                };
            }
            else
            {
                if (genericArguments.Length != typeArgs.Length) return null;

                return new
                {
                    MethodInfo = mi,
                    TypeParameters = genericArguments
                        .Zip(typeArgs, Tuple.Create)
                        .ToDictionary(t => t.Item1, t => t.Item2)
                };
            }
        })
        .Where(a => a != null)
        .Where(a => a.MethodInfo
            .GetParameters()
            .Select(pi => pi.ParameterType)
            .SequenceEqual(args.Select(o => o.GetType()), new EqualsComparer<Type>((x, y) =>
                (x.IsGenericParameter)
                    ? a.TypeParameters[x].IsAssignableFrom(y)
                    : x.Equals(y)))
        )
        .ToArray();

    if (!typedMethods.Any()) throw new ArgumentException(string.Format("\"{0}\" not match arguments : Type <{1}>", methodName, typeof(T).Name));

    // nongeneric
    var nongeneric = typedMethods.Where(a => a.TypeParameters == null).ToArray();
    if (nongeneric.Length == 1) return nongeneric[0].MethodInfo;

    // generic--
    var lessGeneric = typedMethods
        .Where(a => !a.MethodInfo.GetParameters().All(pi => pi.ParameterType.IsGenericParameter))
        .ToArray();

    // generic
    var generic = (typedMethods.Length == 1)
        ? typedMethods[0]
        : (lessGeneric.Length == 1 ? lessGeneric[0] : null);

    if (generic != null) return generic.MethodInfo.MakeGenericMethod(generic.TypeParameters.Select(kvp => kvp.Value).ToArray());

    // ambiguous
    throw new ArgumentException(string.Format("\"{0}\" ambiguous arguments : Type <{1}>", methodName, typeof(T).Name));
}

private class EqualsComparer<TX> : IEqualityComparer<TX>
{
    private readonly Func<TX, TX, bool> equals;

    public EqualsComparer(Func<TX, TX, bool> equals)
    {
        this.equals = equals;
    }

    public bool Equals(TX x, TX y)
    {
        return equals(x, y);
    }

    public int GetHashCode(TX obj)
    {
        return 0;
    }
}

泥臭い。LINQ的には普段あまり使わないGroupJoinや、SequenceEqualでのIEqualityComparerとか、ここぞとばかりに色々仕込んで実に楽しげになりました。何とも酷いゴリ押し。速度とかどうなのこれ、ただリフレクションを使っただけじゃないよね、というのは、UnitTestですから。だから、許容される。そうでなければ、やれない。

名前からマッチ->型の当てはめ->実引数からマッチ->非ジェネリックメソッドを優先->引数が全てジェネリックでなければ優先->最終的にメソッドが一つにまで絞り込めなければエラー。コンパイラの行う、正確なオーバーロードの解決法はC#言語仕様書の7.5.3に書いてあります。従っていません。というか、outとかrefとか非対応だし、ジェネリックに関しても持ってる情報が足りなすぎてマッチしたくてもできない。特に痛いのはコード上での型が吹っ飛んでいて、GetTypeによる具象型しか取得できないこと。そのせいで、引数の複数の同一のTは、ほぼ同じ型同士でないとダメになってしまっていて(コード上で宣言しているインターフェイスの情報が取れないのでどうしょもない)。その他、入れ子なジェネリックの場合もコケます(対応させるの面倒くさい)。

でも、ふつーの9割がたなシチュエーションでは動作するはずです。

ちなみに、ジェネリックの型引数は通常、DynamicObjectのBinderでは取得出来ません。Binderの実際の型(の持つインターフェイス)であるICSharpInvokeOrInvokeMemberBinderのTypeArgumentsが持っているのですが、ICSharpInvokeOrInvokeMemberBinderがinternalのため、外側から手出しは出来ないのです。どうするかって、もうここまで来たら何も躊躇うことなくリフレクションです本当にありがとうございました。GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");とか、負けたにも程がある。

そんなわけで、ふつーに作ると存外面倒くさいっぽいです。いや、こんな泥臭くなるのは何かおかしい気はかなりしなくもないんですが、うーん。まあ、泥臭くてもライブラリ側で吸収出来るなら、それはそれでいいかな、と。結果だけを見れば。ライブラリが泥臭さを担保するかわりに、ユーザーはAsDynamic()だけで綺麗に呼び出せる。それはとっても嬉しいなって。

余談

そんなDynamicAccessorのテスト作るのにIsとかAssertEx.Throws、あって良かった。大変助かった。ExpectedExceptionAttributeなんて使ってられない。どっぐふーどどっぐふーど。個人的にはMSTestは凄い好きというか、テストツール選ぶのにあたって優先度が一番高い項目はIDE統合(デバッグ含むというかデバッグ最重要)なので、統合できてないものはその時点でアウトです(どれも、一手間加えれば統合出来るのでしょうけれど)。その上で、Chaining Assertionを使えばMSTestの色々な不満が一気に解消出来て、最高に幸せだなあ、と、自画自賛。

まとめ

当初のIsだけでサクッと軽量なテスト~とかってノリは何処に行ったんでしょうか。ぐぬぬ。それでも、最大限のシンプルさは保ち続けている、と、思いたい。内部がどうあれ、外からはAsDynamic()が足されただけだし、それ自体もシンプルそのものですよね、ね?コンセプトはまだ守れてると、思いたい。

ところで、dynamic使ってます?ぶっちけ全然使ってません。結局のところvarが最高に便利なわけで、dynamicは、例えば以前書いたDynamicJsonであったり、これのようなリフレクションであったりと、通常のC#とは違う場所との糊なわけで、そうそう出番のあるものでもない、ですねん。C#4.0の言語的な追加の最たるものはdynamicなわけですが、普段はそんな使わない代物なわけだと、言語的にはC#4.0はあんま変わらなかったねー、という印象で。LL的な視点から、C#にはdynamicで動的言語でもあるんだって?という意見をたまに見ますが、純C#上ではぶっちゃけほとんど使わないのでそんなでもない。

じゃあなくてもいいか、というと、んー、まあ、このように、たまにある分には便利だし、言語的にもスムースに入り込んでいるので、良いのではないか、むしろ良いのではないか、とは思います。あんま使わないけどたまには思い出してあげると大変可愛い。

そういえば私はメタ構文変数としてhoge, huga, hageの順にhogehogeと使ってるのですが、Twiterでhomuにする。というのを見て、なんかいいな、とか思ってしまったので、当分はhomu, mogu, mamiの順に使おうかと思っている昨今。こーいうのは半年後ぐらいに、あちゃーという気持ちになるのが常なのですがー。

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、是非試してみてください。

C#(.NET)のテストフレームワーク4種の比較雑感

for MSTestをやめて、NUnitとMBUnitとxUnit.NETにも対応しました。MSTestに限定していたのは、単純に他のを入れて試すの面倒くせー、というだけの話であり、そういう態度はいけないよね、と思ったので全部入れました。NUnitはDocumentだけは読んでかなり参考にしてたのですが、他のは全くはぢめて。MSTest以外はみんな野心的に開発進んでるんですね。比べると機能面では一番見劣りするMSTest。

というわけで、対応させるために各種フレームワークを入れる&多少触ったので、それらの紹介/感想などを書きたいと思います。C#上というか.NET上、ですね。の前に、更新事項が幾つかあるのでそれを。まず、CollectionAssertに等値比較するラムダ式を受けるオーバーロードを追加しました。

var lower = new[] { "a", "b", "c" };
var upper = new[] { "A", "B", "C" };

// IEqualityComparer<T>かFunc<T,T,bool>を渡して、コレクションの任意の演算子で等値比較
lower.Is(upper, StringComparer.InvariantCultureIgnoreCase);
lower.Is(upper, (x, y) => x.ToUpper() == y.ToUpper());

// Linq to ObjectsのSequenceEqualを使えば今までも出来なくもなかったのですが!
lower.SequenceEqual(upper, StringComparer.InvariantCultureIgnoreCase).Is(true);

SequenceEqual使えばいいぢゃん(キリッ って思ってました。Isのオーバーロードがかなり嵩んでいて、オーバーロードのIntelliSense汚染が始まってる。IntelliSense = ヘルプ。であるべきなのですが、数が多かったり似たようなオーバーロードが多いと、混乱を招いてしまい良くないわけです。だいたいがして、以前にAllを拒んでいたのにSequenceEqualはアリなのかよ、という。一応弁解すると、CollectionAssert自体に比較子を受けるオーバーロードがあるので、IsがCollectionAssertを示すのなら、それを使える口を用意すること自体は悪くないかな、と。あと、SequenceEqualはIEqualityComparerなのが使いづらい。ラムダ式を渡せる口がないので、こちらに用意して回避したかったというのもあります。AnonymousComparer - lambda compare selector for Linqを使えばラムダ式渡せますが。

それにしても、本来のものはIComparerを受けるんですよ、ジェネリックじゃないほうの。今時ノンジェネリックを強要とか、しかもIComparer<T>とIComparerは別物なのでIComparer<T>として定義すると使えないという。ありえない!そもそも何故にIComparerなのか、という話ではある。大小比較じゃないので、等値として0しか判定に使ってませんもの。それならIEqualityComparer<T>だろ、と。GetHashCodeの定義が(不要なのに)面倒だから、そのせいかなー。 そう考えると分からなくもないので、Func<T, T, bool>を用意しておきました。ラムダ式は最高よね。

それと、今回、コレクションでのIsの動作を変更しました。今までは欠陥があって、例えばlower.Is(upper)とすると、オーバーロードがかち合ってCollectionAssertではなくAreEqualsのほうで動いてしまってました。これは、大変望ましくない。オーバーロードだけだと解決しようがないので、内部で動的にIEnumerbaleかを判定した上でCollectionAssertを使うようにしてやりました。

例外のテスト

他のテストフレームワークを色々見たのですが、例外は属性じゃなくてラムダ式でのテストをサポートするのも少なくない。というかMSTestだけですが、それがないのは。属性だと、範囲が広すぎる(メソッドをテストしたいのに、コンストラクタがテスト範囲に含まれ、コンストラクタが例外を吐いてしまったらメソッドのテストにならない)問題があるので、ラムダ式で書けるほうがいい、という流れのようで。その他に1例外テスト1メソッドが強制されて面倒くさいー、引数のチェックぐらい、1メソッドに全部突っ込んでおきたい、というモノグサな私的にもラムダ式のほうが好み。というわけで、移植移植。

// Throes<T>で発生すべき例外を指定する
AssertEx.Throws<ArgumentNullException>(() => "foo".StartsWith(null));

// 戻り値も取得可能で、その場合は発生した例外が入ってます
var ex = AssertEx.Throws<InvalidOperationException>(() =>
{
    throw new InvalidOperationException("foobar operation");
});
ex.Message.Is(s => s.Contains("foobar")); // それにより追加のアサーションが可能

// 例外が起きないことを表明するテスト
AssertEx.DoesNotThrow(() =>
{
    // code
});

これを入れるのに伴い、拡張メソッド置き場のクラス名をAssertExに変更しました。それに加え、partial classにしたので、独自に何かメソッド置きたい場合に、AssertExに追加することが可能です。.csファイル一つをポン置きでライブラリと言い張るからこそ出来るやり口……。割とどうでもいい。

テストフレームワーク雑感

Assertちょっと触った程度でしかないので、本当に雑感ですが、一応は各フレームワークを触ったので感想など。

非常に何とかUnitっぽい仕上がり。おお、これこれ、みたいな。ある種のスタンダード。他のテストフレームワークにあるあの機能が欲しいなあ、みたいなものもしっかり取り入れてる感で、全く不足なく。情報も豊富、周辺環境も充実。

不満は、Assert.That。これJavaのJUnit発祥なのかしらね。Hamcrest?まあしかしともかく、酷い。英文のようで読みやすいとか入力補完ですらすらとか、ないない。これが本当に読みやすい?書きやすい?ありえないでしょ……。Is.All.GreaterThanとか、ただのシンタックス遊び。ラムダ式のないJava(or .NET2.0)ならともかく、現在のC#でそれやる意味はどこにもない。

かなり独自な方向に走っている印象。Gallioというプラットフォーム中立なテストシステムを立てて、その上にNUnitやMSTest、そしてGallioの前身でありテストフレームワークのMbUnitなどが乗っかる。という、壮大なお話。IronRubyでRSpec、など独自にテストシステムを立てられるほどに需要がなさそうな、でもあると絶対嬉しいと思う人いるよね、といったものを一手に吸収出来るかもです(実際RSpecは乗っかってる模様)。そんな壮大な話を出すだけあって、テストランナーとしてのGallioの出来はかなり良いように見えます。

MbUnit自体は可もなく不可もなく。属性周りとかは独特なのかなあ、単純なアサート関数しか使っていないので、その辺は分かりません。

ちなみに、今回紹介するテストフレームワークの中で、唯一NuGet非対応。対応に関しては議論されたようですが、どうもGallioのプラグインを中心とする依存関係が、現在のNuGetだと上手く対応させられないそうで。将来のNuGetでも対応するような仕組みへの変更は今のところ考えてない、とNuGet側から返答を貰っているみたいなので、当面はMbUnitのNuGet入りはなさそうです、残念。まあ、若干大掛かりなGallioのインストール込みで考えたほうが嬉しいことも多く、NuGet経由での必要性は薄いから、それはそれでしょうがないかな、といったところ。

非常に独特で、旧来のテストの記述法を徹底的に見直して新しく作り直されています。とっつきは悪いのですが(如何せん、他と比べて全然互換がない)良く考えられていて、素晴らしいと思います。

例えば、CollectionAssertはなく、Assert.Equalが万能に、Objectの場合はEqualsで、Collectionの場合は列挙しての値比較で行ってくれます。つまり、この辺はChaining AssertionのIsと同じ。旧来のしがらみに囚われず、Assert関数はどういう形であることがベストなのか、ということを突き詰めて考えると、そうなる。と思う。

ただ、非常に厳密な比較を行うので、型が違う(IEnumerable<T>とT[]とか)とFailになります。Chaining AssertionのIsは、ゆるふわに列挙後の値比較だけで判定します。どちらが良いのかは、正しいテスト、ということを考えればxUnit.NETのほうなのでしょう。私は、その辺は、とにかく「書き味」優先で振りました。型比較の厳密さは例外テストのThrowsメソッドにも現れていて、MSTestやMbUnitは派生型も含め一致すればSuccessとしますが、xUnit.NETは厳密に一致しないとFailになります。Chaining AssertionのThrowsは厳密一致のほうを採用しました。

正しいテストを書くために、テストフレームワークはかくあるべき、という強い意志でもって開発されている感じ。これは、相当に良いと思います。MSTest以外のものを使うなら、私はこれを選びたい。付属のテストランナーは貧弱ですが、Gallioを使うことで克服出来ます。

  • MSTest

唯一TestCaseがない。これがたまらなく不便なんです!かわりに強力なデータソース読み込みが可能になっているようですけれど、強力な分、セットアップに手間がかかってダルいという。他は、いたって普通。ふつーのAssert関数群とふつーの属性でのテスト設定。このノーマルっぷりは標準搭載らしいかもです。最大の欠点はウィザードで自動生成されるテンプレコードがどうしょうもなくクソなこと。あのノイズだらけのゴミは何とかすべし。

最大のメリットは完全なVisual Studio統合。サクッとデバッグ実行。何て素晴らしい。標準搭載で準備一切不要なのも嬉しい。昨今のテストの重要度を考えると、Express EditionにもMSTest入ってるべきですねえ。ちょっと弱いAssert関数群とTestCaseがないことは、Chaining Assertionを使えば補完されるので、全体的に割と問題なくなって素敵テストフレームワークに早変わり。TestCaseに関してはモドきなので若干弱いけれど。

番外編。僕と契約してPexをより強固にしようよ!Code Contracts + Pexは後ろにMicrosoft Researchがいる.NETならではの強烈さだなあ、と。とりあえずVS2010 後の世代のプログラミング (2) Pex « kazuk は null に触れてしまったの素晴らしい導入記事を読んで試せばいいと思うんだ。そういえば、Pex開発者はMbUnitのプロジェクト創設者(学生時代にMbUnit作って、その後MSRに行ったそうで、凄いね...)だそうですよ。

比較しての相違点

他のテストフレームワークや拡張補助ライブラリと根源的に異なるのは、CollectionAssertに対する考え方です。Linq to Objectsに投げればそれが一番でしょ?という。末尾に.Isなのは、それが一番Linq to Objectsを適用して返した場合に書きやすいから。Linqはインフラ。これを活用しないなんて勿体無い。ドキュメントにそれを書くことで、公的に推奨している、ということを押し出している、つもりです。

まとめ

xUnit.NETはかなり素晴らしいんじゃないかと思います。IDE完全統合という点で、私はMSTestを選んでしまいますが、MSTest以外から使うものを選択するのならば、xUnit.NETにしたいところ。周辺環境がまるで整ってない感はありますが、その辺はGallioを使えば吸収出来るっぽいので、セットで、みたいなところかしらん。

テストは別に同一言語である必要はない、ので、CLRの特色を活かせば、IronRubyでRSpecというのは魅力的な道に見えます。今回は試せていないのですが、いつか試してみたい。F#を用いてのテストフレームワークも良さそう。F#は、特にテストのような小さい単位ではF# Interactiveに送りながら書けるのが強烈なアドバンテージになるよね。ユニットテストぐらいの小さな単位なら、デバッガよりもむしろこちらのほうが小回り効いて書きやすそう。

私的にはChaining Assertionはそれらと比べても、全く引けを取らない書きやすさがあると思っています。C#はクラス定義などを除けば、コード本体自体の書き味はライトウェイトなのですよ、ね、ね!

それにしてもAssertThat一族の毒はどうにかならないのかねえ……。AssertThatがない、という点だけでxUnit.NET一択ですよほんと。F#などでも、この形式を踏襲しているのを見ると大変モニョる。こういうのがビヘイビアドリブンなんですかね?ただのシンタックス遊びなだけで、そーいうの違くない?って。でもAssertで詳細なデータが出ない?Expression Treeを解析して詳細なデータを出しゃあいいわけで。どっかの言語の仕組みを持ってこないで、C#の持つ特色、強みであるExpression Treeを活かす方向で動けないものなのか。

別にJavaだからどうとか言うつもりではないです。大切なのはあらゆる言語を見つめて、良いものを取り入れることでしょう?例えば、Groovyの素晴らしいPower AssertをC#でやろうとするプロジェクト expressiontocode など。C#に取り込もうとするなら、こちらのやり方でしょう。時系列を考えればそれは違う、という話もあるでしょうけど、現在の、これからの姿勢として。本当に良いものは何なのかを、考えよう。それを実現するための力がC#には備わっているのだから。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive