2012-04-30 36 views
4

我想编写一个单元测试,验证我的路由注册和ControllerFactory,以便给定一个特定的URL,将创建一个特定的控制器。事情是这样的:测试ControllerFactory(预启动初始化阶段)

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

我修改从书“临ASP.NET MVC 3框架”采取了代码,现在看来,这将是完美的除了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的自定义控制器工厂,该工厂的行为我想验证。

回答

1

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

有大量关于测试路由设置的帖子/博客(只是谷歌'mvc测试路线')。这一切都归结于在httpcontext中嘲讽一个请求并调用GetRouteData。

如果你确实需要一些忍者技能来模拟构建管理者:围绕内部接口有一种方法,我使用它来进行(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(); } 
} 
+1

我遇到了同样的问题,试图测试从IOC容器中拉出控制器的自定义控制器工厂。所以我的用例是验证控制器是否从GetControllerInstance()方法中的IOC容器中提取了正确的控制器(由于它是受保护的,显然只能通过CreateController()访问,所以不能直接测试)。但是由于抛出的异常,代码并没有达到那么远。 – Thierry