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

时间:2012-03-09 17:09:48

标签: c# model-view-controller templates testing mocking

我有一个使用显示模板生成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:

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 个答案:

答案 0 :(得分: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)使用自托管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>");
    }
}

备注:

  • 在三个 easy 步骤中,您现在可以测试扩展方法的逻辑
  • 我已经实现了足够的代码来使我的测试工作(即@Model)。如果您需要在视图中提供更丰富的API支持,则必须扩展RazorTemplateBase