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

熟悉 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[].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[].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
}
}

JS&CSS文件请求合并及压缩处理研究(四)的更多相关文章

  1. JS&CSS文件请求合并及压缩处理研究(五)

    接上篇.在我们最终调用 @Html.RenderResFile(ResourceType.Script) 或者 @Html.RenderResFile(ResourceType.StyleSheet) ...

  2. JS&CSS文件请求合并及压缩处理研究(三)

    上篇我们进行了一些代码方面的准备工作.接下来的逻辑是:在View页面解析时,通过 Html.AppendResFile 方法添加的资源文件,我们需要按照分组.优先级,文件名等条件,对其路径进行合并.具 ...

  3. JS&CSS文件请求合并及压缩处理研究(一)

    在我们日常的网站开发工作中,一个页面难免会引用到各种样式及脚本文件.了解Web开发的朋友们都知道,页面引用的每一个: <link href="style.css" rel=& ...

  4. JS&CSS文件请求合并及压缩处理研究(二)

    上篇交待了一些理论方面的东西,并给出了另外一种解决方案的处理流程.本篇将根据该处理流程,开始代码方面的编写工作. 1,打开VS,新建ASP.NET MVC Web项目,项目类型选择空.名称为 Mcmu ...

  5. ASP.NET MVC 4 Optimization的JS/CSS文件动态合并及压缩

    JS/CSS文件的打包合并(Bundling)及压缩(Minification)是指将多个JS或CSS文件打包合并成一个文件,并在网站发布之后进行压缩,从而减少HTTP请求次数,提高网络加载速度和页面 ...

  6. 开箱即用 - Grunt合并和压缩 js,css 文件

    js,css 文件合并与压缩 Grunt 是前端自动化构建工具,类似webpack. 它究竟有多强悍,请看它的 介绍. 这里只演示如何用它的皮毛功能:文件合并与压缩. 首先说下js,css 合并与压缩 ...

  7. Web性能优化之动态合并JS/CSS文件并缓存客户端

    来源:微信公众号CodeL 在Web开发过程中,会产生很多的js/css文件,传统的引用外部文件的方式会产生多次的http请求,从而加重服务器负担且网页加载缓慢,如何在一次请求中将多个文件一次加载出来 ...

  8. 前端js,css文件合并三种方式,bat命令

    前端js,css文件合并三种方式,bat命令 前端js文件该如何合并三个方式如下:1. 一个大文件,所有js合并成一个大文件,所有页面都引用它.2. 各个页面大文件,各自页面合并生成自己所需js的大文 ...

  9. 使用PHP和GZip压缩网站JS/CSS文件加速网站访问速度

    使用PHP和GZip压缩网站JS/CSS文件加速网站访问速度 一些泛WEB 2.0网站为了追求用户体验,可能会大量使用CSS和JS文件.这就导致在服务器带宽一定的情况下,多用户并发访问速度变慢.如何加 ...

随机推荐

  1. 使用pango-Cairo列出系统中的有效字体

    使用pango-Cairo列出系统中的有效字体,代码来源于gtk-app-devel-list fonts list using pango #include <glib.h> #incl ...

  2. iOS 视图,动画渲染机制探究

    腾讯Bugly特约作者:陈向文 终端的开发,首当其冲的就是视图.动画的渲染,切换等等.用户使用 App 时最直接的体验就是这个界面好不好看,动画炫不炫,滑动流不流畅.UI就是 App 的门面,它的体验 ...

  3. javascript 设计模式-----观察者模式

    观察者模式在设计模式中被重点提到,因为它应用的场景非常多,而且在模块化设计当中扮演着非常重要的角色.MVC模式中最底层的就是观察者模式,当下流行的javascript框架backbone就是很好地运用 ...

  4. 【转】 Nginx深入详解之多进程网络模型

    [转自]http://blog.chinaunix.net/uid-22312037-id-3974068.html 一.进程模型        Nginx之所以为广大码农喜爱,除了其高性能外,还有其 ...

  5. 《你必须知道的.NET》读书笔记一:小OO有大智慧

    此篇已收录至<你必须知道的.Net>读书笔记目录贴,点击访问该目录可以获取更多内容. 一.对象  (1)出生:系统首先会在内存中分配一定的存储空间,然后初始化其附加成员,调用构造函数执行初 ...

  6. 【读书笔记】javascript 继承

    在JavaScript中继承不像C#那么直接,C#中子类继承父类之后马上获得了父类的属性和方法,但JavaScript需要分步进行. 让Brid 继承 Animal,并扩展自己fly的方法. func ...

  7. 记录maven java.lang.String cannot be cast to XX error

    在项目开发中自定义了一个maven plugin,在本地能够很好的工作,但是在ci server上却无法正常工作报错为: --------------------------------------- ...

  8. Javascript事件模型系列(三)jQuery中的事件监听方式及异同点

    作为全球最知名的js框架之一,jQuery的火热程度堪称无与伦比,简单易学的API再加丰富的插件,几乎是每个前端程序员的必修课.从读<锋利的jQuery>开始,到现在使用jQuery有一年 ...

  9. java提高篇(五)-----抽象类与接口

    接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 抽象类与接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的面向对象的能力.他们两者之间对抽象概念 ...

  10. html5 Web Workers

    虽然在JavaScript中有setInterval和setTimeout函数使javaScript看起来好像使多线程执行,单实际上JavaScript使单线程的,一次只能做一件事情(关于JavaSc ...