4

特定の URL を指定すると、特定のコントローラーが作成されるように、ルート登録と ControllerFactory を検証する単体テストを作成したいと考えています。このようなもの:

Assert.UrlMapsToController("~/Home/Index",typeof(HomeController));

本「Pro ASP.NET MVC 3 Framework」から抜粋したコードを変更しましたが、ControllerFactory.CreateController() 呼び出しが InvalidOperationException をスローして、This method cannot be called during the application's pre-start initialization stage.

そこで、MVC ソース コードをダウンロードしてデバッグし、問題の原因を探しました。これは、潜在的なコントローラーを見つけることができるように、参照されているすべてのアセンブリを探している ControllerFactory から発生します。CreateController コール スタックのどこかで、特定の問題を引き起こすコールは次のとおりです。

internal sealed class BuildManagerWrapper : IBuildManager {
    //...

    ICollection IBuildManager.GetReferencedAssemblies() {
        // This bails with InvalidOperationException with the message
        // "This method cannot be called during the application's pre-start 
        // initialization stage."
        return BuildManager.GetReferencedAssemblies();
    }

    //...
}

これに関するSOの解説を見つけました。上記のコードを満足させるために手動で初期化できるものがあるかどうかはまだ疑問です。誰?

しかし、それがなければ...呼び出しが IBuildManager の実装から来ていることに気付かずにはいられません。独自の IBuildManager を注入する可能性を探りましたが、次の問題に遭遇しました。

  • IBuildManager は とマークされinternalているので、そこから他の承認された派生物が必要です。アセンブリSystem.Web.Mvc.Testには、テスト シナリオ用に設計された というクラスがありMockBuildManager、これは完璧です!!! これは 2 番目の問題につながります。
  • 私が知る限り、配布可能な MVC には、System.Web.Mvc.Test アセンブリ (DOH!) が付属していません。
  • MVC 配布可能ファイルが System.Web.Mvc.Test アセンブリに付属していたとしても、インスタンスを持つことMockBuildManagerは解決策の半分にすぎません。そのインスタンスを にフィードすることも必要DefaultControllerFactoryです。残念ながら、これを達成するためのプロパティ セッターもマークされていますinternal(DOH!)。

要するに、MVC フレームワークを「初期化」する別の方法を見つけない限り、私の選択肢は次のいずれかです。

  • 元の問題を回避できるように、DefaultControllerFactory とその依存関係のソース コードを完全に複製しますGetReferencedAssemblies()。(うーん!)
  • 配布可能な MVC を、MVC ソース コードに基づいて独自のビルドの MVC に完全に置き換えます - いくつかのinternal修飾子を削除するだけです。(ダブルうっ!)

ちなみに、MvcContrib「TestHelper」が私の目標を達成しているように見えることは知っていますが、実際の IControllerFactory を使用してコントローラーのタイプ/インスタンスを取得するのではなく、リフレクションを使用してコントローラーを見つけるだけだと思います。

このテスト機能が必要な大きな理由は、動作を確認したい DefaultControllerFactory に基づいてカスタム コントローラー ファクトリを作成したからです。

4

1 に答える 1

1

ここで何を達成しようとしているのかよくわかりません。ルート設定をテストするだけの場合。内部にハッキングするよりも、それをテストするだけの方がはるかに優れています。TDD の第 1 のルール: 記述したコードのみをテストします (この場合、それはルーティングのセットアップであり、MVC によって行われる実際のルート解決手法ではありません)。

ルート設定のテストに関する投稿/ブログがたくさんあります (「mvc test route」をググってください)。最終的には、httpcontext でリクエストをモックし、GetRouteData を呼び出します。

buildmanager をモックするために忍者のスキルが本当に必要な場合は、(LinqPad) 実験的テストに使用する内部インターフェイスを回避する方法があります。現在、ほとんどの .net アセンブリには InternalsVisibleToAttribute セットがあり、おそらく別の署名済みテスト アセンブリを指しています。この属性のターゲット アセンブリをスキャンし、名前 (および公開キー トークン) に一致するアセンブリをオンザフライで作成することにより、内部に簡単にアクセスできます。

個人的には、本番環境のテスト コードではこの手法を使用しないことに注意してください。しかし、複雑なアイデアを切り離すには良い方法です。

void Main()
{
    var bm = BuildManagerMockBase.CreateMock<MyBuildManager>();
    bm.FileExists("IsCool?").Dump();
}

public class MyBuildManager : BuildManagerMockBase
{
    public override bool FileExists(string virtualPath) { return true; }
}

public abstract class BuildManagerMockBase
{
    public static T CreateMock<T>() 
        where T : BuildManagerMockBase
    {
        // Locate the mvc assembly
        Assembly mvcAssembly = Assembly.GetAssembly(typeof(Controller));

        // Get the type of the buildmanager interface
        var buildManagerInterface = mvcAssembly.GetType("System.Web.Mvc.IBuildManager",true);

        // Locate the "internals visible to" attribute and create a public key token that matches the one specified.
        var internalsVisisbleTo = mvcAssembly.GetCustomAttributes(typeof (InternalsVisibleToAttribute), true).FirstOrDefault() as InternalsVisibleToAttribute;
        var publicKeyString = internalsVisisbleTo.AssemblyName.Split("=".ToCharArray())[1];
        var publicKey = ToBytes(publicKeyString);

        // Create a fake System.Web.Mvc.Test assembly with the public key token set
        AssemblyName assemblyName = new AssemblyName();
        assemblyName.Name = "System.Web.Mvc.Test";
        assemblyName.SetPublicKey(publicKey);

        // Get the domain of our current thread to host the new fake assembly
        var domain = Thread.GetDomain();
        var assemblyBuilder = domain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
        moduleBuilder = assemblyBuilder.DefineDynamicModule("System.Web.Mvc.Test", "System.Web.Mvc.Test.dll");
        AppDomain currentDom = domain;
        currentDom.TypeResolve += ResolveEvent;

        // Create a new type that inherits from the provided generic and implements the IBuildManager interface
        var typeBuilder = moduleBuilder.DefineType("Cheat", TypeAttributes.NotPublic | TypeAttributes.Class, typeof(T), new Type[] { buildManagerInterface });      
        Type cheatType = typeBuilder.CreateType();

        // Magic!
        var ret = Activator.CreateInstance(cheatType) as T;

        return ret;
    }

    private static byte[] ToBytes(string str)
    {
        List<Byte> bytes = new List<Byte>();

        while(str.Length > 0)
        {
            var bstr = str.Substring(0, 2);
            bytes.Add(Convert.ToByte(bstr, 16));
            str = str.Substring(2);
        }

        return bytes.ToArray();
    }

    private static ModuleBuilder moduleBuilder;

    private static Assembly ResolveEvent(Object sender, ResolveEventArgs args)
    {
        return moduleBuilder.Assembly;
    }

    public virtual bool FileExists(string virtualPath)      { throw new NotImplementedException(); }
    public virtual Type GetCompiledType(string virtualPath) { throw new NotImplementedException(); }
    public virtual ICollection GetReferencedAssemblies()    { throw new NotImplementedException(); }
    public virtual Stream ReadCachedFile(string fileName)   { throw new NotImplementedException(); }
    public virtual Stream CreateCachedFile(string fileName) { throw new NotImplementedException(); }
}
于 2012-05-23T07:22:16.113 に答える