如何在mvc3中使用razor语法测试视图?
我正在编写测试C#MVC3应用程序的代码。我可以测试控制器,但是如何测试视图中的代码?这包括JavaScript和剃刀风格的代码。如何在mvc3中使用razor语法测试视图?
是否有任何工具可用于模拟C#中的视图或测试视图和JavaScript?
以下是关于测试视图的渲染输出。例如,可以将此文本输出加载到DOM中,以便使用XPath进行进一步分析(对于XHTML使用XmlReader
或对SGML样式的HTML使用HtmlAgilityPack
)。通过一些好的帮助方法,可以轻松检查视图的特定部分,例如测试//a[@href='#']
或其他任何您想测试的部分。这有助于使单元测试更加稳定。
当使用Razor而不是“吹起来的”WebForms引擎时,人们会认为这很容易,但事实证明,相反,由于许多Razor视图引擎和视图使用部件的内部工作原理HtmlHelper
尤其是)HTTP请求生命周期。事实上,正确地测试生成的输出需要很多运行代码才能获得可靠和合适的结果,如果使用诸如便携式区域(来自MVCContrib项目)等特殊内容等等,那么更加如此。
操作和URL的HTML帮助程序要求路由已正确初始化,路由字典已正确设置,控制器也必须存在,并且还有其他与加载视图数据有关的“陷阱”,例如设置视图数据字典...
我们最终创建了一个ViewRenderer
类,它将实际在您的Web的物理路径上实例化一个应用程序主机以进行测试(可以静态缓存,每个单一测试重新初始化由于初始化滞后而不切实际):
host = (ApplicationHost)System.Web.Hosting.ApplicationHost.CreateApplicationHost(typeof(ApplicationHost), "/", physicalDir.FullName);
ApplicationHost
类继承自MarshalByRefObject
,因为主机将加载到单独的应用程序域中。主机执行各种不圣洁的初始化东西,以便正确初始化HttpApplication
(注册路由等的global.asax.cs
中的代码),同时禁用某些方面(如身份验证和授权)。 被警告,严重的黑客入侵。使用风险自负。
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
方法使用反射来使内存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
是一个“空” TextWriter
,其存储应用程序实例。我无法找到另一种方式来初始化应用程序实例并获取它。它的核心非常简单。
public override void Close() {
Flush();
}
public override void Flush() {
if ((applicationInstance == null) && (HttpContext.Current != null)) {
applicationInstance = HttpContext.Current.ApplicationInstance;
}
}
现在,这使我们能够呈现出几乎任何(剃刀)查看,就好像它是在一个真正的web服务器托管,为使其产生几乎完整的HTTP生命周期:
private static readonly Regex rxControllerParser = new Regex(@"^(?<areans>.+?)\.Controllers\.(?<controller>[^\.]+)Controller$", RegexOptions.CultureInvariant|RegexOptions.IgnorePatternWhitespace|RegexOptions.ExplicitCapture);
public string RenderViewToString<TController, TModel>(string viewName, bool partial, Dictionary<string, object> 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<TController>(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<TModel> viewDataDictionary = new ViewDataDictionary<TModel>(model);
if (viewData != null) {
foreach (KeyValuePair<string, object> 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();
}
}
也许这帮助你获得一些结果。总的来说,很多人认为测试视图的麻烦并不值得。我会让你成为那个评委。