• 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
  • 相关阅读:
    [分享]解决Ubuntu 16.04安装Vitis 2019.2失败的问题
    ZCU102 休眠到内存(suspend-to-ram)对DDR复位信号的设计
    PetaLinux使用bitbake提前下载所有软件包
    Ubuntu 16.04执行基本命令失败,恢复Python版本后正常
    Upgrade Zynq-7000 XIP reference design to Xilinx SDK 2018.3
    区块链入门到实战(26)之以太坊(Ethereum) – 挖矿
    区块链入门到实战(25)之以太坊(Ethereum) – 以太币单位
    区块链入门到实战(24)之以太坊(Ethereum) – 网络节点
    区块链入门到实战(23)之以太坊(Ethereum) – 虚拟机架构
    区块链入门到实战(22)之以太坊(Ethereum) – 账号(地址)
  • 原文地址:https://www.cnblogs.com/mcmurphy/p/3343151.html
Copyright © 2020-2023  润新知