如何测试依赖于显示模板的代码



我有一个使用显示模板生成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)向扩展方法类添加可扩展性挂钩:

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)使用自托管Razor引擎执行模板:

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)覆盖测试中的默认扩展方法Renderer

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

最新更新