C# / Moles で dynamic を使う (Chaining Assertion を使って private メンバのテストを書きたい)

実は最近 C# でちょっと動作確認がめんどくさいコードをぺけぺけ書いている。
というわけで、珍しく(?)できるだけ手厚くテストを書こうとしているのだが、外部のライブラリとかサービスとか絡んでいて、まさにテスト泣かせな状況。


そう、今こそレガシーコード本(WEwLC, レガシーコード改善ガイド)を読んだ経験が生きるとき! なんか Seam とか Sprout とかあったよなあ……と本をひっくり返しそうになったが、今は Moles といういいものがありまして。


Moles は MS 謹製のモックフレームワークで、一言で言うと「任意のメソッドの振る舞いをアドホックに書き換えることができる」という最終兵器的な代物。
例えば、中で現在日時の取得を行なっているコードをテストしようと思ったら、その日時取得部分を関数オブジェクトとか delegate とかまあなんしかそこらへんの手段で外出しして……みたいなことが通常は必要になってくるわけだが、Moles なら日時取得が決まった日付を返す実装に一時的に書き換えてからテストを走らせるという芸当が、元のコードを一切変更せずにできる。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Moles.Framework;
[assembly: MoledType(typeof(System.DateTime))]

//  (中略)

    [TestMethod()]
    [HostType("Moles")]
    public void DateTest()
    {
        System.Moles.MDateTime.NowGet = () => new DateTime(2000, 1, 1); // DateTime.Now を差し替え

        Assert.AreEqual(DateTime.Now, new DateTime(2000, 1, 1)); // 現在日付によらず 2000/1/1 が返ってくる
    }

DBアクセスをモックに差し替えるのとかも、ラッパークラスとか無しで実現できる。これはかなり嬉しい。


一方。
VisualStudio の unit test framework では、上のサンプルのように Assert.AreEqual とか書かないといけないのだが、これが冗長で少々めんどくさい。
Chaining Assertion を使えば、これが

        DateTime.Now.Is(new DateTime(2000, 1, 1));

のように簡潔で直感的に書けるようになる。めっちゃいい。


そういえばレガシーコード本の読書会で、アクセス修飾子が private なメンバをどうやってテストするか、という問題がたびたび話題にあがるたびに、private なものはそもそもテストの対象外と考えるべき、どうしてもやりたいならリフレクションでね(めんどくさいけど!)、という結論で、いやまあそうなんだけど、設計的には private な奴だけど、内部の働きが正しいかどうか確認するためにどうしてもテストしたいんだよ! const 属性とかできる処理系ならそういう方向もあるだろうけど、Java とか const ないし〜、ということがやっぱりままある(と思う)。
Java だとアクセス修飾子無しにすることで、ある種 package scope とでも呼べるものにしておいて、テストを同じバッケージに配置する、ということをして、まあこれでできるけどなんか不自然だよなあ、やっぱりリフレクション使うしかないか? いやでもそれはめんどくさい……というジレンマで無駄にヤル気を摩滅させるという香ばしい状態に陥ってみたりするわけだが、C# 4.0(DLR) + Chaining Assertion なら AsDynamic というメソッドを経由して、private メンバのテストがなんとも簡潔に書けてしまう。

    public class PrivateClass
    {
        private int value;         // private member
        public PrivateClass(int v)
        {
            value = v * 2;
        }
    }

// (中略)

        [TestMethod()]
        public void PrivateTest()
        {
            var obj = new PrivateClass(3);

            int v = obj.AsDynamic().value;  // private member の value を取得
            v.Is(6);
        }


dynamic に対して型推論が効かないので、明示的に型を指定した変数に一度代入したり、キャストしたりしないと Is できないのが唯一残念だが(Chaining じゃあない!)、まあそれは贅沢すぎる文句だろう。
今までとてつもなくめんどくさいことをしないと書けなかったテストがこんなに簡潔に書けるのだから。


というわけで、やっとここから本題。
そんな Moles と Chaining Assersion の AsDynamic を同時に使って うはうはテストを書こうとすると、こういうエラーに直面することになる。

テスト メソッド ****.****.**** が例外をスローしました:
System.InvalidOperationException: 動的な操作は、同種の AppDomain でのみ実行できます。


実は Chaining Assersion / AsDynamic に限らず、[HostType("Moles")] をつけたテストメソッドの中で dynamic を使おうとすると、このエラーが出る。
このエラーメッセージ、実はある意味「誤訳」で、元の英語では次のようなメッセージになっている。

System.InvalidOperationException: Dynamic operations can only be performed in homogenous AppDomain


homogenous を「同種の」と訳すのは別に間違ってないようにも思えるが、このリファレンスを見たら気が変わる。

AppDomain.IsHomogenous Property

This property returns true for sandboxed application domains that were created by using the AppDomain.CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) method overload. Sandboxed application domains have a homogenous set of permissions; that is, the same set of permissions is granted to all partially trusted assemblies that are loaded into the application domain. A sandboxed application domain optionally has a list of strong-named assemblies that are exempt from this permission set, and instead run with full trust.


同じリファレンスの和訳。MSDN によくある機械訳ではなく、人間訳。

AppDomain.IsHomogenous プロパティ

このプロパティは、AppDomain.CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) メソッド オーバーロードを使用して作成されたサンドボックスで保護されたアプリケーション ドメインに対して true を返します。 サンドボックスで保護されたアプリケーション ドメインには、同じアクセス許可セットがあります。つまり、アプリケーション ドメインに読み込まれる部分的に信頼されたアセンブリすべてに同じアクセス許可セットが与えられます。 サンドボックスで保護されたアプリケーション ドメインには、このアクセス許可セットから除外され、代わりに完全信頼で実行される厳密名付きのアセンブリの一覧があることがあります。


つまり AppDomain が homogeneous であるとは、「アクセス許可セットが同じ」という概念であるということがわかる。なのに、エラーメッセージではただ「同種の」と訳されてはこのリファレンスにたどり着くことも出来ず、何をどうしたら良いのかわからない。


話を戻して。
正しいエラーメッセージがわかれば、解決方法を見つけることもできる。


要は C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\ にある Microsoft.Moles.VsHost.x86.exe.Config と Microsoft.Moles.VsHost.exe.Config について*1、次のように2箇所の true を false に書き換えればよい。
Program Files 下なので、当然通常の権限では書き換えられないから、notepad などを管理者権限で立ち上げて編集する必要がある。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="false">
    <supportedRuntime version="v4.0" />
    <supportedRuntime version="v2.0.50727" />
  </startup>
  <runtime>
     <legacyCasPolicy enabled="false" />
  </runtime>
</configuration>


これで Moles と Chaining Assersion の AsDynamic を同時に使えるようになる。めでたし。
デフォルトのポリシーがなぜ微妙に厳しくなっているのかわからないため、書き換えてしまっていいのかなあという気が少ししないでもないが、任意のメソッドを書き換えられるくせに dynamic を使わせないとか意味分かんないので、まあきっといいんだろう、ということにしておく。

*1:プラットフォームに合わせてどちらか片方を書き換えればいいが、わかんなかったら両方書き換えても構わない