【问题标题】:Test ControllerFactory (pre-start initialization stage)测试ControllerFactory(预启动初始化阶段)
【发布时间】:2012-05-23 15:49:15
【问题描述】:

我想编写一个单元测试来验证我的路由注册和 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();
    }

    //...
}

I found a SO commentary on this。我仍然想知道是否有可以手动初始化的东西来让上面的代码开心。任何人?

但是如果没有那个...我不禁注意到调用来自 IBuildManager 的实现。我探索了the possibility of injecting my own 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 的自定义控制器工厂,我想验证它的行为。

【问题讨论】:

    标签: asp.net asp.net-mvc asp.net-mvc-3 unit-testing


    【解决方案1】:

    我不太确定您要在这里完成什么。如果它只是测试您的路线设置;你最好只测试它而不是侵入内部结构。 TDD 的第一条规则:只测试您编写的代码(在这种情况下,这是路由设置,而不是 MVC 完成的实际路由解析技术)。

    有大量关于测试路由设置的帖子/博客(只是谷歌的“mvc 测试路由”)。这一切都归结为在 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(); }
    }
    

    【讨论】:

    • 我遇到了同样的问题,试图测试从 IOC 容器中拉出控制器的自定义控制器工厂。所以我的用例是验证控制器是否在 GetControllerInstance() 方法中从 IOC 容器中拉出正确的控制器(因为它受到保护,所以不能直接测试,显然只能通过 CreateController() 访问)。但是由于抛出异常,代码并没有走那么远。
    猜你喜欢
    • 1970-01-01
    • 2012-07-08
    • 2015-05-13
    • 2020-11-08
    • 2013-11-30
    • 1970-01-01
    • 1970-01-01
    • 2017-02-03
    • 1970-01-01
    相关资源
    最近更新 更多