2012-03-09 76 views
3

我有一個使用顯示模板來生成HTML實用方法:如何測試依賴於顯示模板的代碼?

public static MvcHtmlString MyMethod(this HtmlHelper html) 
{ 
    var model = new Model(); 

    var viewDataContainer = new ViewDataContainer<INode>(model); 
    var modelHtmlHelper = new HtmlHelper<INode>(html.ViewContext, 
                 viewDataContainer); 

    return modelHtmlHelper.DisplayFor(node => node, "TemplateName"); 
} 

我試圖寫一個測試,以驗證其行爲。到目前爲止,我已經拿出:

public class when_extension_method_is_used 
{ 
    static MvcHtmlString output; 

    Because of =() => 
    { 
     var httpContext = new Mock<HttpContextBase>(); 
     httpContext.SetupGet(hc => hc.Items).Returns(new ListDictionary()); 

     var routeData = new RouteData(); 
     routeData.Values.Add("controller", "Test"); 

     var viewContext = new ViewContext 
     { 
      RouteData = routeData, 
      HttpContext = httpContext.Object, 
      ViewData = new ViewDataDictionary() 
     }; 

     var viewDataContainer = new ViewPage(); 

     var htmlHelper = new HtmlHelper(viewContext, viewDataContainer); 

     output = htmlHelper.MyMethod(); 
    }; 

    It should_just_work = 
     () => output.ToString().ShouldEqual("<blink></blink>"); 
    } 
} 

這是行不通的。我得到一個NullReferenceException在:

at System.Web.Compilation.BuildManager.GetVPathBuildResultFromCacheInternal(VirtualPath virtualPath, Boolean ensureIsUpToDate) 
at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate) 
at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile, Boolean throwIfNotFound, Boolean ensureIsUpToDate) 
at System.Web.Compilation.BuildManager.GetObjectFactory(String virtualPath, Boolean throwIfNotFound) 
at System.Web.Mvc.BuildManagerWrapper.System.Web.Mvc.IBuildManager.FileExists(String virtualPath) in BuildManagerWrapper.cs: line 8 
at System.Web.Mvc.BuildManagerViewEngine.FileExists(ControllerContext controllerContext, String virtualPath) in BuildManagerViewEngine.cs: line 42 
at System.Web.Mvc.VirtualPathProviderViewEngine.GetPathFromGeneralName(ControllerContext controllerContext, List`1 locations, String name, String controllerName, String areaName, String cacheKey, String[]& searchedLocations) in VirtualPathProviderViewEngine.cs: line 180 
at System.Web.Mvc.VirtualPathProviderViewEngine.GetPath(ControllerContext controllerContext, String[] locations, String[] areaLocations, String locationsPropertyName, String name, String controllerName, String cacheKeyPrefix, Boolean useCache, String[]& searchedLocations) in VirtualPathProviderViewEngine.cs: line 167 
at System.Web.Mvc.VirtualPathProviderViewEngine.FindPartialView(ControllerContext controllerContext, String partialViewName, Boolean useCache) in VirtualPathProviderViewEngine.cs: line 113 
at System.Web.Mvc.ViewEngineCollection.<>c__DisplayClass8.<FindPartialView>b__7(IViewEngine e) in ViewEngineCollection.cs: line 97 
at System.Web.Mvc.ViewEngineCollection.Find(Func`2 lookup, Boolean trackSearchedPaths) in ViewEngineCollection.cs: line 66 
at System.Web.Mvc.ViewEngineCollection.Find(Func`2 cacheLocator, Func`2 locator) in ViewEngineCollection.cs: line 48 
at System.Web.Mvc.ViewEngineCollection.FindPartialView(ControllerContext controllerContext, String partialViewName) in ViewEngineCollection.cs: line 96 
at System.Web.Mvc.Html.TemplateHelpers.ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, String templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) in TemplateHelpers.cs: line 66 
at System.Web.Mvc.Html.TemplateHelpers.TemplateHelper(HtmlHelper html, ModelMetadata metadata, String htmlFieldName, String templateName, DataBoundControlMode mode, Object additionalViewData, ExecuteTemplateDelegate executeTemplate) in TemplateHelpers.cs: line 239 
at System.Web.Mvc.Html.TemplateHelpers.TemplateHelper(HtmlHelper html, ModelMetadata metadata, String htmlFieldName, String templateName, DataBoundControlMode mode, Object additionalViewData) in TemplateHelpers.cs: line 192 
at System.Web.Mvc.Html.TemplateHelpers.TemplateFor(HtmlHelper`1 html, Expression`1 expression, String templateName, String htmlFieldName, DataBoundControlMode mode, Object additionalViewData, TemplateHelperDelegate templateHelper) in TemplateHelpers.cs: line 181 
at System.Web.Mvc.Html.TemplateHelpers.TemplateFor(HtmlHelper`1 html, Expression`1 expression, String templateName, String htmlFieldName, DataBoundControlMode mode, Object additionalViewData) in TemplateHelpers.cs: line 174 
at System.Web.Mvc.Html.DisplayExtensions.DisplayFor(HtmlHelper`1 html, Expression`1 expression, String templateName) in DisplayExtensions.cs: line 43 

例外的來源是System.Web.VirtualPath.GetCacheKey()

public string GetCacheKey() 
{ 
    // VirtualPathProvider property is null 
    return HostingEnvironment.VirtualPathProvider.GetCacheKey(this); 
} 
  • 有沒有一種方法來初始化HostingEnvironment.VirtualPathProvider
  • 如果沒有,是否有更好的方法來測試依賴於顯示模板的代碼?

回答

1

創建解決方法。這是醜陋的,違反最佳做法,但工程。

1)可擴展性掛鉤加入擴展方法類:

public static class MyExtensionMethods 
{ 
    static MyExtensionMethods() 
    { 
     Renderer = (html, model) => 
     { 
      // this is the default implementation that will be used by MVC runtime 
      var viewDataContainer = new ViewDataContainer<INode>(model); 
      var modelHtmlHelper = new HtmlHelper<INode>(html.ViewContext, viewDataContainer); 

      return modelHtmlHelper.DisplayFor(node => node, "TemplateName"); 
     }; 
    } 

    public static Func<HtmlHelper, INode, MvcHtmlString> Renderer { get; set; } 

    public static MvcHtmlString Menu(this HtmlHelper html) 
    { 
     var model = new Model(); 
     return Renderer(html, model); 
    } 
} 

2)使用的模板執行自託管剃刀引擎:

using System; 
using System.CodeDom.Compiler; 
using System.IO; 
using System.Linq; 
using System.Reflection; 
using System.Text; 
using System.Web.Razor; 
using Microsoft.CSharp; 

namespace YourNamespace.Specifications.SpecUtils 
{ 
    public sealed class InMemoryRazorEngine 
    { 
     public static ExecutionResult Execute<TModel>(string razorTemplate, TModel model, params Assembly[] referenceAssemblies) 
     { 
      var razorEngineHost = new RazorEngineHost(new CSharpRazorCodeLanguage()); 
      razorEngineHost.DefaultNamespace = "RazorOutput"; 
      razorEngineHost.DefaultClassName = "Template"; 
      razorEngineHost.NamespaceImports.Add("System"); 
      razorEngineHost.DefaultBaseClass = typeof(RazorTemplateBase<TModel>).FullName; 

      var razorTemplateEngine = new RazorTemplateEngine(razorEngineHost); 

      using (var template = new StringReader(razorTemplate)) 
      { 
       var generatorResult = razorTemplateEngine.GenerateCode(template); 

       var compilerParameters = new CompilerParameters(); 
       compilerParameters.GenerateInMemory = true; 
       compilerParameters.ReferencedAssemblies.Add(typeof(InMemoryRazorEngine).Assembly.Location); 
       if (referenceAssemblies != null) 
       { 
        foreach (var referenceAssembly in referenceAssemblies) 
        { 
         compilerParameters.ReferencedAssemblies.Add(referenceAssembly.Location); 
        } 
       } 

       var codeProvider = new CSharpCodeProvider(); 
       var compilerResult = codeProvider.CompileAssemblyFromDom(compilerParameters, generatorResult.GeneratedCode); 

       var compiledTemplateType = compilerResult.CompiledAssembly.GetExportedTypes().Single(); 
       var compiledTemplate = Activator.CreateInstance(compiledTemplateType); 

       var modelProperty = compiledTemplateType.GetProperty("Model"); 
       modelProperty.SetValue(compiledTemplate, model, null); 

       var executeMethod = compiledTemplateType.GetMethod("Execute"); 
       executeMethod.Invoke(compiledTemplate, null); 

       var builderProperty = compiledTemplateType.GetProperty("OutputBuilder"); 
       var outputBuilder = (StringBuilder)builderProperty.GetValue(compiledTemplate, null); 
       var runtimeResult = outputBuilder.ToString(); 

       return new ExecutionResult(generatorResult, compilerResult, runtimeResult); 
      } 
     } 

     #region Nested type: ExecutionResult 

     public sealed class ExecutionResult 
     { 
      public ExecutionResult(GeneratorResults generatorResult, CompilerResults compilerResult, string runtimeResult) 
      { 
       GeneratorResult = generatorResult; 
       CompilerResult = compilerResult; 
       RuntimeResult = runtimeResult; 
      } 

      public GeneratorResults GeneratorResult { get; private set; } 
      public CompilerResults CompilerResult { get; private set; } 
      public string RuntimeResult { get; private set; } 
     } 

     #endregion 

     #region Nested type: RazorTemplateBase 

     public abstract class RazorTemplateBase<TModel> 
     { 
      protected RazorTemplateBase() 
      { 
       OutputBuilder = new StringBuilder(); 
      } 

      public TModel Model { get; set; } 
      public StringBuilder OutputBuilder { get; private set; } 

      public abstract void Execute(); 

      public virtual void Write(object value) 
      { 
       OutputBuilder.Append(value); 
      } 

      public virtual void WriteLiteral(object value) 
      { 
       OutputBuilder.Append(value); 
      } 
     } 

     #endregion 
    } 
} 

3)覆蓋默認擴展方法渲染器在您的測試中

public class when_extension_method_is_used 
{ 
    static MvcHtmlString output; 

    Because of =() => 
    {  
     var htmlHelper = new HtmlHelper(new ViewContext(), new ViewPage()); 

     MyExtensionMethods.Renderer = (html, model) => 
     { 
      const string template = "<blink>@Model</blink>"; 

      var executionResult = InMemoryRazorEngine.Execute(template, model); 
      return new MvcHtmlString(executionResult.RuntimeResult); 
     }; 

     output = htmlHelper.MyMethod(); 
    }; 

    It should_just_work = 
     () => output.ToString().ShouldEqual("<blink>make UX experts cry!</blink>"); 
    } 
} 

注:

  • 在其後容易步驟,你現在就可以測試你的擴展方法的邏輯
  • 我已經實現了足夠的代碼讓我的測試工作(即@Model) 。如果您需要在視圖中提供更豐富的API支持 - 您將不得不延長RazorTemplateBase