Ниже приведено тестирование визуализированного вывода представления.Этот текстовый вывод может, например, быть загружен в DOM для дальнейшего анализа с XPath (используя XmlReader
для XHTML или HtmlAgilityPack
для HTML в стиле SGML).С некоторыми хорошими вспомогательными методами это позволяет легко проверять определенные части представления, такие как тестирование //a[@href='#']
или что-либо еще, что вы хотите протестировать.Это помогает сделать модульные тесты более стабильными.
Можно было бы ожидать, что это легко при использовании Razor вместо «взорванного» движка WebForms, но, как оказалось, из-за многих внутреннихработы механизма представления Razor и представления с использованием частей (особенно HtmlHelper
) жизненного цикла HTTP-запроса.Фактически, для правильного тестирования сгенерированного вывода требуется много работающего кода для получения надежных и подходящих результатов, особенно если вы используете в соединении экзотические вещи, такие как переносимые области (из проекта MVCContrib) и тому подобное.
HTML-помощники для действий и URL-адресов требуют, чтобы маршрутизация была правильно инициализирована, словарь маршрутов был правильно настроен, контроллер также должен существовать, и есть другие "ошибки", связанные с загрузкой данных для представления, такие как настройкасловарь представления данных ...
Мы закончили тем, что создали класс ViewRenderer
, который фактически будет создавать экземпляр хоста приложения на физическом пути вашей сети для тестирования (может быть статически кэширован, повторная инициализация для каждогоодиночный тест нецелесообразен из-за задержки инициализации):
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
использует отражение, чтобы сделать изменяемую веб-конфигурацию в памяти:
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), как если бы оно было размещено на реальном веб-сервере, создавая почти полный жизненный цикл 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();
}
}
Может быть, это поможет вам достичь некоторых результатов.В общем, многие люди говорят, что хлопоты по проверке просмотров не стоят усилий.Я позволю тебе быть судьей этого.