How can I test code that depends on display templates?

I have a utility method that uses Display Templates to generate 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");
}

      

I am trying to write a test to test its behavior. So far I have come up with:

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>");
    }
}

      

This does not work. I am getting a NullReferenceException

at:

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

      

The source of the exception is System.Web.VirtualPath.GetCacheKey()

:

public string GetCacheKey()
{
    // VirtualPathProvider property is null
    return HostingEnvironment.VirtualPathProvider.GetCacheKey(this);
}

      

  • Is there a way to initialize HostingEnvironment.VirtualPathProvider

    ?
  • If not, is there a better way to test the code that depends on the display templates?
+3


source to share


1 answer


Created a workaround. It's ugly and violates best practices, but it works.

1) Add extension class for extension method class:

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) Use a self-driving Razor engine to execute the template:

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) Override the default Renderer extension method in your test

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>");
    }
}

      

Notes:

  • And in three easy steps you can test the logic of your extension methods
  • I have executed enough code for my tests to work (namely @Model). If you want richer API support in views, you will have to extendRazorTemplateBase

+1


source







All Articles