如何在mvc3中使用razor语法测试视图?

我正在编写代码来测试C#MVC3应用程序。 我可以测试控制器但是如何测试视图中的代码? 这包括javascript和剃刀风格的代码。

有没有可用的工具可以在C#中模拟视图或测试视图和javascript?

以下是关于测试视图的渲染输出。 例如,可以将此文本输出加载到DOM中,以便使用XPath进行进一步分析(使用XmlReader for XHTML或HtmlAgilityPack for SGML-style HTML)。 使用一些不错的帮助方法,可以方便地检查视图的特定部分,例如测试//a[@href='#']或您要测试的任何其他内容。 这有助于使unit testing更稳定。

人们可以期待在使用Razor而不是“爆炸式”WebForms引擎时这很容易,但事实certificate是相反的,因为Razor视图引擎的许多内部工作和使用部件的视图(特别是HtmlHelper ) HTTP请求生命周期。 实际上,正确测试生成的输出需要大量运行代码才能获得可靠和适当的结果,如果你使用混合中的可移植区域(来自MVCContrib项目)之类的奇特东西,则更是如此。

操作和URL的HTML帮助程序要求正确初始化路由,正确设置路由字典,控制器也必须存在,并且还有其他与为视图加载数据相关的“陷阱”,例如设置视图数据字典……

我们最终创建了一个ViewRenderer类,它实际上将在您要测试的Web物理路径上实例化一个应用程序主机(可以静态缓存,由于初始化延迟,每个单个测试的重新初始化是不切实际的):

 host = (ApplicationHost)System.Web.Hosting.ApplicationHost.CreateApplicationHost(typeof(ApplicationHost), "/", physicalDir.FullName); 

ApplicationHost类inheritance自MarshalByRefObject因为主机将加载到单独的应用程序域中。 主机执行各种不正常的初始化操作,以便正确初始化HttpApplicationglobal.asax.cs中注册路由等的代码),同时禁用某些方面(例如身份validation和授权)。 警告,严重的黑客攻击。 使用风险由您自己承担。

 public ApplicationHost() { ApplicationMode.UnitTesting = true; // set a flag on a global helper class to indicate what mode we're running in; this flag can be evaluated in the global.asax.cs code to skip code which shall not run when unit testing // first we need to tweak the configuration to successfully perform requests and initialization later AuthenticationSection authenticationSection = (AuthenticationSection)WebConfigurationManager.GetSection("system.web/authentication"); ClearReadOnly(authenticationSection); authenticationSection.Mode = AuthenticationMode.None; AuthorizationSection authorizationSection = (AuthorizationSection)WebConfigurationManager.GetSection("system.web/authorization"); ClearReadOnly(authorizationSection); AuthorizationRuleCollection authorizationRules = authorizationSection.Rules; ClearReadOnly(authorizationRules); authorizationRules.Clear(); AuthorizationRule rule = new AuthorizationRule(AuthorizationRuleAction.Allow); rule.Users.Add("*"); authorizationRules.Add(rule); // now we execute a bogus request to fully initialize the application ApplicationCatcher catcher = new ApplicationCatcher(); HttpRuntime.ProcessRequest(new SimpleWorkerRequest("/404.axd", "", catcher)); if (catcher.ApplicationInstance == null) { throw new InvalidOperationException("Initialization failed, could not get application type"); } applicationType = catcher.ApplicationInstance.GetType().BaseType; } 

ClearReadOnly方法使用reflection使内存中的Web配置变为可变:

 private static void ClearReadOnly(ConfigurationElement element) { for (Type type = element.GetType(); type != null; type = type.BaseType) { foreach (FieldInfo field in type.GetFields(BindingFlags.Instance|BindingFlags.NonPublic|BindingFlags.DeclaredOnly).Where(f => typeof(bool).IsAssignableFrom(f.FieldType) && f.Name.EndsWith("ReadOnly", StringComparison.OrdinalIgnoreCase))) { field.SetValue(element, false); } } } 

ApplicationCatcher是一个“null” TextWriter ,用于存储应用程序实例。 我找不到另一种方法来初始化应用程序实例并获取它。 它的核心非常简单。

 public override void Close() { Flush(); } public override void Flush() { if ((applicationInstance == null) && (HttpContext.Current != null)) { applicationInstance = HttpContext.Current.ApplicationInstance; } } 

现在,这使我们能够呈现几乎任何(Razor)视图,就像它被托管在真实的Web服务器中一样,创建几乎完整的HTTP生命周期来呈现它:

 private static readonly Regex rxControllerParser = new Regex(@"^(?.+?)\.Controllers\.(?[^\.]+)Controller$", RegexOptions.CultureInvariant|RegexOptions.IgnorePatternWhitespace|RegexOptions.ExplicitCapture); public string RenderViewToString(string viewName, bool partial, Dictionary viewData, TModel model) where TController: ControllerBase { if (viewName == null) { throw new ArgumentNullException("viewName"); } using (StringWriter sw = new StringWriter()) { SimpleWorkerRequest workerRequest = new SimpleWorkerRequest("/", "", sw); HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current = new HttpContext(workerRequest)); RouteData routeData = new RouteData(); Match match = rxControllerParser.Match(typeof(TController).FullName); if (!match.Success) { throw new InvalidOperationException(string.Format("The controller {0} doesn't follow the common name pattern", typeof(TController).FullName)); } string areaName; if (TryResolveAreaNameByNamespace(match.Groups["areans"].Value, out areaName)) { routeData.DataTokens.Add("area", areaName); } routeData.Values.Add("controller", match.Groups["controller"].Value); ControllerContext controllerContext = new ControllerContext(httpContext, routeData, (ControllerBase)FormatterServices.GetUninitializedObject(typeof(TController))); ViewEngineResult engineResult = partial ? ViewEngines.Engines.FindPartialView(controllerContext, viewName) : ViewEngines.Engines.FindView(controllerContext, viewName, null); if (engineResult.View == null) { throw new FileNotFoundException(string.Format("The view '{0}' was not found", viewName)); } ViewDataDictionary viewDataDictionary = new ViewDataDictionary(model); if (viewData != null) { foreach (KeyValuePair pair in viewData) { viewDataDictionary.Add(pair.Key, pair.Value); } } ViewContext viewContext = new ViewContext(controllerContext, engineResult.View, viewDataDictionary, new TempDataDictionary(), sw); engineResult.View.Render(viewContext, sw); return sw.ToString(); } } 

也许这有助于您获得一些结果。 一般来说,很多人都说测试视图的麻烦并不值得。 我会让你做出判断。

查看vantheshark的文章,它描述了如何使用NSubstitute模拟ASP.NET MVC View引擎。