• 对ASP.NET MVC项目中的视图做单元测试


    作者:赵劼

    时间:2009-02-25

    关于视图的单元测试

      说到ASP.NET MVC,我们似乎始终都在关注对于Controller的测试 -- -- 虽然Stephen Walther也写过如何脱离Web Server对View进行单元测试,但是他的方法可看而不可用。复杂的构造和预备,以及对生成的HTML字符串作判断 -- -- 这真是在对视图做单元测试吗?仔细分析他的代码可以发现,这其实是在对ViewEngine做单元测试。而且,如果真要对ViewEngine做单元测试,也不应该像他那样依赖外部文件。在我看来,他的做法什么都不是......似乎美观,似乎能博得一些"掌声",但是这个掌声是来自于他的解决方案,还是大家一时的冲动呢?

      如果要对视图做单元测试,还是要将内容呈现在浏览器中才行。在对网页做单元测试时,我们一般会使用WatiN等工具操作浏览器,打开页面,再对其DOM元素结构及内容作断言。不过......这是单元测试吗?可惜这只能算是一种回归测试或用户验收测试。因为,我们在打开一个页面的时候,从表现层到业务逻辑再到数据访问,应用程序的每个部件都在忙碌着。而单元测试讲究的是"分离",分离一切关注,分离一切依赖。因为分离,我们才能准确定位错误;因为分离,我们才能在测试中使用我们准备好的数据。

      既然要分离,我们就必须遵循一定的使用规范。在《ASP.NET MVC单元测试最佳实践》中我提到,在View中只能使用ViewData中的数据,而不该依赖其他内容(包括HttpContext)。这样我们就可以自行构造ViewData并注入一个视图对象中。事实上,这个约定在ASP.NET MVC自带的项目模板中就被破坏了。请看Views\Shared\LogOnUserControl.ascx,其中通过this.User来查看当前用户的登陆状态。这是个定义在传统Page对象上的属性,从当前HttpContext上直接获取。如果使用这种方式,我们在单元测试时就难以"模拟"当前用户的登陆状态,进而难以使测试覆盖到测试的各种情况了。

    Lightweight Test Automation Framework

      在这里,老赵推荐使用ASP.NET Team提供的Lightweight Test Automation Framework(下文称之为LTAF)作为测试工具,它目前已经在CodePlex上更新至Feb Update版本。这个框架的作用与WatiN和Selenium类似,可操作浏览器对应用程序编写回归测试。虽然在某些方面(例如DOM元素的选取)不如"竞争对手",但是LTAF自有其独到之处:

    由于直接在浏览器中运行,它天生便支持现有的 -- -- 以及未来可能出现的任意浏览器。

    由于直接部署在被测试的网站中,因此测试代码和网站页面是在同一个进程中。

      第一点优势自不必说,而第二点更是关键。试想WatiN和Selenium,都是通过编写代码在浏览器中打开页面。这意味着我们的在测试代码和被测试的网页分别在不同的进程中。在这个前提下,如果我们要将测试代码中定义的数据传递给被测试的网页(也就是视图对象),我们就必须进行跨进程的通信。而无论怎么实现,都逃不过"序列化"一途,这无疑增加了复杂度。而使用LTAF之后,这个问题瞬间烟消云散了,因为我们可以直接在内存中"传递"测试数据,一切都只是个引用而已。

      不过任何事物都具有两面性,LTAF也有一些难以天生的,而且是永远无法弥补的缺点。例如:

    由于LTAF将待测试的页面放置在Frame中,因此该页面上的window.top等基于浏览器frame结构的属性会被改变。

    由于LTAF的本质是使用JavaScript来操作DOM,这意味着任何会阻塞程序进行的操作(例如alert)都不能使用,否则将阻塞整个测试过程。

      不过幸运的是,这两点都不回成为严重的问题。对于第一种,我们只需要编写一个自定的getTop方法来替换直接访问windows.top的做法即可。而第二种情况 -- -- 老赵从来不喜欢alert或confirm这种"纯浏览器功能",因为它们会带来很差的用户体验,更何况现在的JavaScript类库/框架都能很轻松的做出这种效果,您觉得呢?

      LTAF的具体使用方式可参考其Release Note。令人奇怪的是,老赵发现直接在项目中使用LTAF会有一些小问题(不过它的示例为什么就一切正常呢?),因此进行了一些细微的修改。请注意~\UnitView\DriverPage.aspx文件尾部的一些JavaScript代码。

    UnitView的使用

      于是老赵编写了一个组件UnitView,方便我们构造一个单元测试时所需的数据。有了数据,便能够直接将视图在浏览器中加以呈现了。例如:

    1. [WebTestClass]
    2. public class HomeTests
    3. {
    4. [WebTestMethod]
    5. public void LoggedOnIndexTest()
    6. {
    7. var data = new TestViewData<IndexModel>
    8. {
    9. ControllerName = "Home",
    10. ActionName = "Index",
    11. Model = new IndexModel
    12. {
    13. Message = "Welcome guys!",
    14. Identity = new UserIdentity
    15. {
    16. IsAuthenticated = true,
    17. Name = "Jeffrey Zhao"
    18. }
    19. }
    20. };
    21. HtmlPage page = new HtmlPage(TestViewData.GenerateHostUrl(data));
    22. // Assert title
    23. Assert.AreEqual("Home Page", page.Elements.Find("title", 0).GetInnerText());
    24. // Assert head element
    25. var mainContent = page.Elements.Find("main");
    26. var head2 = mainContent.ChildElements.FindAll("h2").Single();
    27. Assert.AreEqual(data.Model.Message, head2.GetInnerText(), "Message should be displayed.");
    28. var loginTabInnerText = page.Elements.Find("logindisplay").GetInnerTextRecursively();
    29. Assert.IsTrue(loginTabInnerText.Contains("Welcome"), "'Welcome' missed.");
    30. Assert.IsTrue(loginTabInnerText.Contains(data.Model.Identity.Name), "Login name missed.");
    31. }
    32. }

      自然,Web Server是不可或缺的。幸运的是,分离让我们的视图只会涉及最简单的测试数据,这样VS自带的简单Web Server就足够了。在上面的代码中,我们直接构造了强类型的TestViewData对象,它包含呈现一个视图所需要的所有数据:

    • Cotroller和Action名称。从理论上说,由不同的Controller和Action进入同样的视图可能会得到不同的结果。
    • View和Master名称。如果省略,则表明将使用默认的视图,即通过Controller和Action的值来确定。
    • ViewData和Model。

      TestViewData.GenerateHostUrl方法会把data保存起来,并返回一个URL。访问该URL便能够得到对应的视图内容。

      如果您想使用UnitView,可以从上面的链接中下载UnitView的源代码和示例在本机进行尝试。使用UnitView时主要有以下几个注意点:

    1. 将Tests项目的输出路径指向被测试网站的bin目录,这样既可以在运行时得到正确的程序集,又不必为网站添加多余的引用。
    2. 将~\UnitView目录复制到您的网站根目录下(在发布网站时,请剔除该目录)。如果想使用其它目录,请关注接下来UnitView实现分析。
    3. 编辑~\UnitView\Web.config文件,将MvcApp.Tests.dll修改为您自己的包含测试代码的程序集。

    UnitView实现分析

      UnitView组件非常简单,简单地几乎不值一提。TestViewData类型包含了测试需要的所有数据,而TestViewData<TModel>继承了TestViewData,提供了强类型的Model属性访问方式。它们就不作分析了。

      此外,TestViewData还有一些静态方法:

    1. public class TestViewData
    2. {
    3. static TestViewData()
    4. {
    5. PersistentProvider = new InProcPersistentProvider();
    6. }
    7. public static IPersistentProvider PersistentProvider { get; set; }
    8. public static string GenerateHostUrl(TestViewData data)
    9. {
    10. var key = PersistentProvider.Save(data);
    11. return ViewHostHandlerUrl + "?key=" + HttpUtility.UrlEncode(key);
    12. }
    13. private static string ViewHostHandlerUrl
    14. {
    15. get
    16. {
    17. return ConfigurationManager.AppSettings["UnitView_ViewHostHandlerUrl"]
    18. ?? "/UnitView/ViewHostHandler.ashx";
    19. }
    20. }
    21. internal static TestViewData Load(string key)
    22. {
    23. return PersistentProvider.Load(key);
    24. }
    25. ...
    26. }

      GenerateHostUrl 方法将委托PersistentProvider 保存对象,并得到一个key 。这个key 将拼接在ViewHostHandlerUrl 属性上,这便是被测试的路径。从代码中可以看出,如果您不想使用默认的测试路径,只需在web.config 的AppSettings 节点中添加一个目标地址即可。

      PersistentProvider 属性为IPersistentProvider 接口类型,其中定义了Save/Load/Remove 三个方法。IPersistentProvider 在项目中只有一个实现:InProcPersistentProvider ,它会将TestViewData 存放在内存中的一个字典里。这个实现已经足够让UnitView 结合LTAF 运行(LTAF 的同进程特性起到了关键的作用)。不过,如果您还是希望使用WatiN 等独立进程的测试工具,就必须实现自己的IPersistentProvider 类型。例如您可以实现一个FilePersistentProvider ,将TestViewData 序列化至一个外部文件中,这样就可以在合适的时候将它取回了。

      另一个较为关键的类型是UnitView.Engine.ViewHostHandler :

    1. public class ViewHostHandler : IHttpHandler
    2. {
    3. private HttpContext Context { get; set; }
    4. public void ProcessRequest(HttpContext context)
    5. {
    6. this.Context = context;
    7. ControllerContext controllerContext = new ControllerContext(
    8. new HttpContextWrapper(context),
    9. this.Data.RouteData,
    10. new MockController());
    11. new ViewResult
    12. {
    13. MasterName = this.Data.MasterName,
    14. ViewName = this.Data.ViewName,
    15. TempData = this.Data.TempData,
    16. ViewData = this.Data.ViewData,
    17. }.ExecuteResult(controllerContext);
    18. }
    19. private string Key
    20. {
    21. get
    22. {
    23. string key = this.Context.Request.QueryString["key"];
    24. if (String.IsNullOrEmpty(key))
    25. {
    26. throw new ArgumentNullException("key");
    27. }
    28. return key;
    29. }
    30. }
    31. private TestViewData m_data;
    32. private TestViewData Data
    33. {
    34. get
    35. {
    36. if (this.m_data == null)
    37. {
    38. this.m_data = TestViewData.Load(this.Key);
    39. if (this.m_data == null)
    40. {
    41. throw new ArgumentNullException("Cannot retrieve the data.");
    42. }
    43. }
    44. return this.m_data;
    45. }
    46. }
    47. public bool IsReusable { get { return false; } }
    48. }

      首先,在ProcessRequest 方法会取回TestViewData ,并根据这些数据构造一个ViewResult 对象,最后执行它的ExecuteResult 方法来输出视图内容。由于ExecuteRequest 方法的需要,我们还必须构造一个ControllerContext 对象,也就意味着我们还必须提供一个Controller 对象和HttpContext 的封装。从代码中可以看出,我们这里使用了最简单的数据。由于视图遵守“ 约定” ,它只会从ViewData 中获取数据,所以无论Controller 或HttpContext 是什么值都已经无关紧要了。

      您可能会想,为什么会有这样的“ 约定” ,不让视图从HttpContext 对象中获取数据呢?Mock 一个HttpContext 对象也不是那么困难(这里要感谢各种强大的Mock 框架)啊。可惜,Mock 后的HttpContext 很难进行序列化,这样就几乎杜绝了跨进程通信的可能,这对于使用WatiN 和Selenium 进行测试的朋友们无疑是一种灾难。权衡之下,老赵决定放弃对HttpContext 的支持。

    注1 :目前UnitView 基于ASP.NET MVC RC 构建,当RTM 发布后我会进行必要的更新。请关注老赵这篇文章和托管在MSDN Code Gallery 上的代码(http://code.msdn.microsoft.com/UnitView )。

    注2 :在《ASP.NET MVC单元测试最佳实践 》中我也包含了UnitView 组件,实现略有不同—— 请以本篇文章为主。

    http://msdn.microsoft.com/zh-cn/dd567692.aspx

  • 相关阅读:
    http协议相关知识
    linux 常用命令总结
    PHP traits
    php 正则案例
    php 中关于正则 元字符
    【U3D】 第三人称控制器ThirdPersonCharacter添加之后角色原地打转不移动的问题(unity5.3.5f)
    .Net Core异步async/await探索
    IdentityServer4实现单点登录统一认证
    CSAPP-Tiny Web服务器【2】源码解析
    CSAPP-Tiny Web服务器【1】编译搭建
  • 原文地址:https://www.cnblogs.com/Cprogrammer/p/2980777.html
Copyright © 2020-2023  润新知