zoukankan      html  css  js  c++  java
  • JS&CSS文件请求合并及压缩处理研究(四)

    本篇将会尝试对之前的代码进行相关的单元测试,验证路径合并规则的覆盖率及正确性。

    熟悉 ASP.NET MVC 开发的朋友应该知道,微软在MVC框架下集成了一款名为 Microsoft.VisualStudio.QualityTools.UnitTestFramework 的单元测试框架。这样我们就不再需要引用第三方诸如NUnit等测试框架了(顺便少受点Java同学的白眼:D)。而 Microsoft.VisualStudio.QualityTools.UnitTestFramework 测试框架的用法,和 NUnit 其实并没有什么大的区别。

    对于ASP.NET MVC 应用程序来说,Controller作为连接View与Model的桥梁,很多时候我们都有必要对其稳定性和正确性创建针对的单元测试。这个过程在MVC中可以很容易的完成。下面我们就实际演示一下。

    定位到Mcmurphy.Tests项目,添加引用:

    (1),Mcmurphy.Web。我们的Controller并没有单独创建项目,而是存放于Mcmurphy.Web项目的Controllers下。所以对Controller的测试需要添加其引用。
    (2),Microsoft.VisualStudio.QualityTools.UnitTestFramework 这是上面提到的微软在MVC中集成的单元测试框架。

    接下来我们在Mcmurphy.Tests项目中,新建 HomeControllerTest.cs文件,添加以下代码:

    using System.Web.Mvc;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using MvcResourceHandle.Controllers;
    namespace Mcmurphy.Tests
    {
        [TestClass]
        public class HomeControllerTest
        {
            [TestMethod]
            public void Index()
            {
                var controller = new HomeController();
                var result = controller.Index() as ViewResult;
                Assert.AreEqual("welcome to chengdu", result.ViewBag.Message);
            }
        }
    }

    然后我们修改 Mcmurphy.Web 项目Controllers目录下的HomeController.cs文件。调整一下 Index Action:

    public ActionResult Index()
    {
           ViewBag.Message = "welcome to chengdu";
           return View();
    }

    接下来我们:

    然后VS会弹出一个测试结果的对话框:

    没错,就是这么简单。换作NUnit,我们只需要将 TestClass => TestFixture,TestMethod => Test。关于NUnit在此不再赘述。相关信息可以百度:TestDriven.NET。从名字就可以看出来,这又是从Java移植过来的哈。

    这里我们用到了 Microsoft.VisualStudio.QualityTools.UnitTestFramework 最常用的两个属性,TestClassAttribute 和 TestMethodAttribute。当然还有一些比较有用的属性,比如 IgnoreAttribute,TimeoutAttribute,TestInitializeAttribute,AssemblyInitializeAttribute等。有兴趣的朋友可以了解下。

    关于单元测试,还有一个比较重要的概念,就是Assert(断言)。顾名思义,就是对某一个结果进行事先的预测和判定。下面列出一些常见的单元测试断言:

    Assert.AreEqual()            //测试指定的值是否相等,如果相等,则测试通过
    Assert.IsTrue()              //测试指定的条件是否为true,如果为true,则测试通过
    Assert.IsFalse()             //测试指定的条件是否为false,如果为false,则测试通过
    Assert.IsNull()              //测试指定的对象是否为空,如果为空,则测试通过
    Assert.IsNotNull()           //测试指定的对象是否非空,如果不为空,则测试通过
    Assert.IsInstanceOfType()     //测试指定的对象是否为某一个类型的实例,如果是,则测试通过

    Okay,对MVC的单元测试有了相应的知识储备之后,接下来我们开始“资源文件路径合并规则”的单元测试。

    由于我们的AppendResFile、RemoveResFile、RenderResFile等方法扩展自 HtmlHelper ,而HtmlHelper对象又是HttpContext相关的。这里又牵涉到另外一个问题,即HttpContext是很难进行模拟的(Mock)。为了提高单元测试的可行性,微软随ASP.NET MVC发布了一个“抽象包”,专门用于对 HttpContext 及其相关组件进行抽象。这里我们会用到这个抽象包里面的 HttpContextBase 和 HttpRequestBase。(对应早先版本的 IHttpContext和 IHttpRequest)。

    先一睹HttpContextBase的源码(部分截图):

    可以看到虽然 HttpContextBase 是一个抽象类,但其实里面的每个方法都有一个默认的实现(throw new NotImplementedException())。这样我们在测试中模拟 HttpContext对象时,只需要继承HttpContextBase实现自己关注的成员即可。

    定位到Mcmurphy.Tests项目,新建 CombineTest.cs 类,添加以下代码:

            /// <summary>
            /// HttpContext模拟类
            /// </summary>
            public class MockHttpContext : HttpContextBase
            {
                //覆写 HttpRequest,便于模拟其它请求信息
                public override HttpRequestBase Request
                {
                    get
                    {
                        return MockRequest;
                    }
                }
    
                public HttpRequestBase MockRequest { get; set; }
    
                IDictionary dict = new Dictionary<string, object>();
                //因为我们将资源文件暂存于 HttpContext.Items 中,所以需要覆写Items
                public override IDictionary Items
                {
                    get { return dict; }
                }
            }
    
            /// <summary>
            /// HttpRequest模拟类
            /// </summary>
            public class MockHttpRequest : HttpRequestBase
            {
                //覆定Form。可以在其中模拟请求数据。
                public override NameValueCollection Form
                {
                    get
                    {
                        return MockForm;
                    }
                }
                public NameValueCollection MockForm { get; set; }
            }    

    对于最终需要模拟的 HtmlHelper,我们看一下它的两个构造函数:

    public HtmlHelper(System.Web.Mvc.ViewContext viewContext, System.Web.Mvc.IViewDataContainer viewDataContainer)
    
    public HtmlHelper(System.Web.Mvc.ViewContext viewContext, System.Web.Mvc.IViewDataContainer viewDataContainer, System.Web.Routing.RouteCollection routeCollection)

    这里我们不需要构造System.Web.Routing.RouteCollection参数。所以选择第一个构造函数。

    因此,我们需要创建 System.Web.Mvc.ViewContext 和 System.Web.Mvc.IViewDataContainer,以满足HtmlHelper对象的创建。

    对于 System.Web.Mvc.IViewDataContainer 接口,直接实例化 System.Web.Mvc.ViewPage 对象,ViewPage 实现了 IViewDataContainer 接口。而实例化ViewPage的前提,则是创建 ViewContext 对象。因此我们可以编写以下代码:

            /// <summary>
            /// 获取HtmlHelper实例
            /// </summary>
            /// <returns></returns>
            private HtmlHelper GetHtmlHelper()
            {
                var page = new ViewPage
                               {
                                   ViewContext = new ViewContext(
                                       new ControllerContext(),
                                       new MyView(""), //自定义视图
                                       new ViewDataDictionary(),
                                       new TempDataDictionary(),
                                       new StringWriter())
                               };
    
                var mockHttpContext = new MockHttpContext();
                var mockHttpRequest = new MockHttpRequest();
                mockHttpContext.MockRequest = mockHttpRequest;
                page.ViewContext.HttpContext = mockHttpContext;
                var htmlHelper = new HtmlHelper(page.ViewContext, page);
                return htmlHelper;
            }

    通过上述方法,我们就可以获取到模拟的 HtmlHelper 对象。但在 ViewContext 的构造函数中,我们传入了 new MyView("") 的参数,也即是自定义 View。这又是个什么东东?

    查看ViewContext的构造函数,我们得知这是一个IView接口类型。IView是MVC中定义视图所需方法的一个接口,其实它也就定义了一个方法 : Render。MSDN的解释为:使用指定的编写器对象来呈现指定的视图上下文。这句话比较绕口。这么说吧,我们常用RazorViewEngine内部就是使用RazorView向页面渲染数据的,而RazorView就是实现了IView接口。SO,如果我们要编写自己的视图引擎,IView的实现是重中之重。下面,我们尝试完成一个简单的 MyView,代码如下:

         /// <summary>
            /// 自定义的视图
            /// 视图需要继承 IView 接口
            /// </summary>
            public class MyView : IView
            {
                // 视图文件的物理路径
                private readonly string _viewPhysicalPath;
    
                public MyView(string viewPhysicalPath)
                {
                    _viewPhysicalPath = viewPhysicalPath;
                }
    
                /// <summary>
                /// 实现 IView 接口的 Render() 方法
                /// </summary>
                public void Render(ViewContext viewContext, TextWriter writer)
                {
                    // 获取视图文件的原始内容  
                    string rawContents = File.ReadAllText(_viewPhysicalPath);
    
                    // 根据自定义的规则解析原始内容  
                    string parsedContents = Parse(rawContents, viewContext.ViewData);
    
                    // 呈现出解析后的内容
                    writer.Write(parsedContents);
                }
    
                public string Parse(string contents, ViewDataDictionary viewData)
                {
                    // 对 {##} 之间的内容作解析
                    return Regex.Replace
                    (
                        contents,
                        @"{#(.+)#}",
    
                        // 委托类型 public delegate string MatchEvaluator(Match match)
                        p => GetMatch(p, viewData)
                    );
                }
    
                protected virtual string GetMatch(Match m, ViewDataDictionary viewData)
                {
                    if (m.Success)
                    {
                        // 获取匹配后的结果,即 ViewData 中的 key 值,并根据这个 key 值返回 ViewData 中对应的 value
                        string key = m.Result("$1");
                        if (viewData.ContainsKey(key))
                        {
                            return viewData[key].ToString();
                        }
                    }
                    return string.Empty;
                }
            }

    上面的MyView仅仅对页面中的“占位符”用ViewData中的值进行了简单的替换。如果我们打算独立的使用这个MyView对页面输出进行渲染,则可以像下面这样操作:

    public ActionResult Index()
            {
                MyView myView = new MyView();
                ViewData["userName"] = "mcmurphy";
                ViewResult result = new ViewResult();
                result.View = myView;
                return result;
            }

    关于自定义视图引擎的更多信息,可以参考:

    http://www.codeproject.com/Articles/294297/Creating-your-own-MVC-View-Engine-into-MVC-Applica

    Okay,切换回文章的Master分支。有了上面的准备工作。下面就可以对之前的路径合并规则进行测试。比如:  

            [TestMethod]
            public void test1()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/A");
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/B");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A,B]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }

    其中 FilterRenderResult 方法是对合并后的路径进行一个简单过滤:

            /// <summary>
            /// 过滤合并后的路径
            /// </summary>
            /// <param name="renderString"></param>
            /// <returns></returns>
            private static string FilterRenderResult(MvcHtmlString renderString)
            {
                var matchs = Regex.Matches(renderString.ToString(), "(?<=<script[^>]*src=['"]?)[^'"> ]*");
                return matchs[1].ToString();
            }

    关于单元测试,其实最主要也最耗时的工作就是测试用例的编写。下面鄙人就贴出完整的CombineTest单元测试类代码,对常用的合并规则进行了测试覆盖。

    using System;
    using System.Collections;
    using System.Collections.Specialized;
    using System.IO;
    using System.Collections.Generic;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Web.Mvc;
    using Mcmurphy.Extension;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Mcmurphy.Component.Enumeration;
    
    namespace Mcmurphy.Tests
    {
        [TestClass]
        public class CombineTest
        {
            #region 测试准备
    
            /// <summary>
            /// HttpContext模拟类
            /// </summary>
            public class MockHttpContext : HttpContextBase
            {
                public override HttpRequestBase Request
                {
                    get
                    {
                        return MockRequest;
                    }
                }
    
                public HttpRequestBase MockRequest { get; set; }
    
                IDictionary dict = new Dictionary<string, object>();
    
                public override IDictionary Items
                {
                    get { return dict; }
                }
            }
    
            /// <summary>
            /// HttpRequest模拟类
            /// </summary>
            public class MockHttpRequest : HttpRequestBase
            {
                public override NameValueCollection Form
                {
                    get
                    {
                        return MockForm;
                    }
                }
                public NameValueCollection MockForm { get; set; }
            }
    
            /// <summary>
            /// 自定义的视图
            /// 视图需要继承 IView 接口
            /// </summary>
            public class MyView : IView
            {
                // 视图文件的物理路径
                private readonly string _viewPhysicalPath;
    
                public MyView(string viewPhysicalPath)
                {
                    _viewPhysicalPath = viewPhysicalPath;
                }
    
                /// <summary>
                /// 实现 IView 接口的 Render() 方法
                /// </summary>
                public void Render(ViewContext viewContext, TextWriter writer)
                {
                    // 获取视图文件的原始内容  
                    string rawContents = File.ReadAllText(_viewPhysicalPath);
    
                    // 根据自定义的规则解析原始内容  
                    string parsedContents = Parse(rawContents, viewContext.ViewData);
    
                    // 呈现出解析后的内容
                    writer.Write(parsedContents);
                }
    
                public string Parse(string contents, ViewDataDictionary viewData)
                {
                    // 对 {##} 之间的内容作解析
                    return Regex.Replace
                    (
                        contents,
                        @"{#(.+)#}",
    
                        // 委托类型 public delegate string MatchEvaluator(Match match)
                        p => GetMatch(p, viewData)
                    );
                }
    
                protected virtual string GetMatch(Match m, ViewDataDictionary viewData)
                {
                    if (m.Success)
                    {
                        // 获取匹配后的结果,即 ViewData 中的 key 值,并根据这个 key 值返回 ViewData 中对应的 value
                        string key = m.Result("$1");
                        if (viewData.ContainsKey(key))
                        {
                            return viewData[key].ToString();
                        }
                    }
                    return string.Empty;
                }
            }
    
            #endregion
    
            #region 辅助方法
    
            /// <summary>
            /// 获取HtmlHelper实例
            /// </summary>
            /// <returns></returns>
            private HtmlHelper GetHtmlHelper()
            {
                var page = new ViewPage
                               {
                                   ViewContext = new ViewContext(
                                       new ControllerContext(),
                                       new MyView(""), 
                                       new ViewDataDictionary(),
                                       new TempDataDictionary(),
                                       new StringWriter())
                               };
    
                var mockHttpContext = new MockHttpContext();
                var mockHttpRequest = new MockHttpRequest();
                mockHttpContext.MockRequest = mockHttpRequest;
                page.ViewContext.HttpContext = mockHttpContext;
                var htmlHelper = new HtmlHelper(page.ViewContext, page);
                return htmlHelper;
            }
    
            /// <summary>
            /// 过滤渲染渲染结果字符串。
            /// 主要是去掉返回结果中的 compress
            /// </summary>
            /// <param name="renderString"></param>
            /// <returns></returns>
            private static string FilterRenderResult(MvcHtmlString renderString)
            {
                var matchs = Regex.Matches(renderString.ToString(), "(?<=<script[^>]*src=['"]?)[^'"> ]*");
                return matchs[1].ToString();
            }
    
            #endregion
    
            #region 测试方法
    
            /// <summary>
            /// 验证同文件夹合并
            /// </summary>
            [TestMethod]
            public void SameFolderText()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/A");
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/B");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A,B]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 验证同文件夹合并
            /// </summary>
            [TestMethod]
            public void SameFolderText1()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "[folderA/A,B][folderA/C,D]", "");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A,B,C,D]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 验证不同分组分开渲染
            /// </summary>
            [TestMethod]
            public void GroupTest()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/A","groupA");
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/B","groupB");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/B]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 验证不同文件夹合并
            /// </summary>
            [TestMethod]
            public void DiffFolderTest1()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/A");
                htmlHelper.AppendResFile(ResourceType.Script, "folderB/A");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A][folderB/A]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 验证不同文件夹合并
            /// </summary>
            [TestMethod]
            public void DiffFolderTest2()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "[folderA/A][folderB/A]");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A][folderB/A]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 验证优先级
            /// </summary>
            [TestMethod]
            public void PriorityTest()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "folderA/A", "");
                htmlHelper.AppendResFile(ResourceType.Script, "folderB/A", "", PriorityType.High);
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderB/A][folderA/A]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 综合测试,
            /// 优先级不同的同文件夹不会合并
            /// </summary>
            [TestMethod]
            public void CompTest1()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "[folderA/A][folderB/A][folderC/A]", "");
                htmlHelper.AppendResFile(ResourceType.Script, "folderB/B", "", PriorityType.High);
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderB/B][folderA/A][folderB/A][folderC/A]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
    
            /// <summary>
            /// 综合测试,
            /// 优先级本同的同文件夹合并
            /// </summary>
            [TestMethod]
            public void CompTest2()
            {
                var htmlHelper = GetHtmlHelper();
                htmlHelper.AppendResFile(ResourceType.Script, "[folderA/A][folderB/A][folderA/B]", "");
                htmlHelper.AppendResFile(ResourceType.Script, "folderB/B");
    
                var renderString = htmlHelper.RenderResFile(ResourceType.Script);
                string result = FilterRenderResult(renderString);
    
                var expectedStr = String.Format("Resource/script?href=[folderA/A,B][folderB/A,B]&compress");
    
                Assert.AreEqual(expectedStr, result);
            }
            #endregion
        }
    }
    View Code
  • 相关阅读:
    【JAVA笔记——道】JAVA对象销毁
    【JAVA笔记——道】并发编程CAS算法
    httpClientUtil的get请求
    python基础 day11 下 ORM介绍 sqlalchemy安装 sqlalchemy基本使用 多外键关联 多对多关系 表结构设计作业
    python基础 day11 上 数据库介绍 mysql 数据库安装使用 mysql管理 mysql 数据类型 常用mysql命令 事务 索引 python 操作mysql ORM sqlachemy学习
    Python基础 Day10 Gevent协程 SelectPollEpoll异步IO与事件驱动 Python连接Mysql数据库操作 RabbitMQ队列 RedisMemcached缓存 Paramiko SSH Twsited网络框架
    python基础 day9 进程、与线程区别 python GIL全局解释器锁 线程 进程
    python基础 day8 Socket语法及相关 SocketServer实现多并发
    python基础 day7 面向对象高级语法部分 异常处理 异常处理 Socket开发基础
    python基础 day6 面向对象的特性:封装、继承、多态 类、方法、
  • 原文地址:https://www.cnblogs.com/mcmurphy/p/3343151.html
Copyright © 2011-2022 走看看