4

我想编写一个单元测试来验证我的路由注册和 ControllerFactory 以便给定特定的 URL,将创建一个特定的控制器。像这样的东西:

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();
    }

    //...
}

我发现了一个关于这个的评论。我仍然想知道是否有可以手动初始化的东西来让上面的代码快乐。任何人?

但是在没有那个的情况下......我不禁注意到调用来自 IBuildManager 的实现。我探索了注入自己的 IBuildManager 的可能性,但遇到了以下问题:

  • IBuildManager 被标记internal,所以我需要一些其他的授权派生。事实证明,该程序集System.Web.Mvc.Test有一个名为 的类MockBuildManager,专为测试场景而设计,非常完美!!!这导致了第二个问题。
  • 据我所知,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 的第一条规则:只测试您编写的代码(在这种情况下,这是路由设置,而不是 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 回答