2012-04-30 62 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