ASP.NET MVC单元测试:HttpContext类的Path属性解惑

开发 后端
HttpContext类中有各种千奇百怪的Path属性,很难掌握。本文谈了一些对于HttpContext类Path属性的一些理解,以便更好的进行单元测试。

有关HttpContext类的Path属性问题描述

前一段时间有朋友在邮件中向我抱怨,说他们团队在使用ASP.NET MVC开发时,在单元测试的时候总是遇到一些不那么方便的地方。例如,对于HttpContext中各种千奇百怪的Path总是无法掌控。例如某个功能会用到HttpContext的Path属性,有的又要用到RawUrl——有的又会涉及到HostName。于是在单元测试的时候,就可能需要填充Mock对象的多种Path属性,而这几种Path属性的值,在理论上还有关系。这其实还是小事,一个麻烦的事情在于,如果功能实现的方式变了,例如原本使用RawUrl属性,而后来忽然觉得应该使用CurrentExecutionFilePath比较合适,于是单元测试就必须跟着改。如此反复,疲于奔命。

就我个人经验看来,这种情况还是蛮常见的,因为某些时候两种Path属性的值差不多,看上去都可以正常使用,于是刚开始编写的时候可能选择了其中一个。但是后来发现,在另一些情况下两种Path就有区别了,而且应该使用的是另一个属性,于是不得不修改,进而单元测试失败了。于是他问我,有没有什么好方法来“完整而可靠地”设置那些缤繁复杂的Path属性。我之前其实也是根据需求设置各种Path属性,但是这的确不好,最重要的问题在于“单元测试”需要了解太多“被测试方法”的实现细节了,这种依赖非常的不可靠。虽然这也是Mock对象被人诟病的特点之一,但是如果我们能够缓解这个缺陷自然再好不过了。

不过话说回来,在“应对”这个问题之前,您要先了解目前的功能是不是真要访问HttpContext中的各种Path。ASP.NET MVC为了提高程序的可测试性作了很多努力,或者说,将“关注点”进行了很大程度的分离。在大部分情况下,我们都能够不去触及HttpContext,而且我们应该尽可能避免这种情况的发生。例如,对Controller做单元测试的时候直接传递参数,为Model Binder做单元测试的时候使用ValueProvider。想来想去,会直接使用到HttpContext的Path属性的场景不多,可能自定义Route算是一个吧,因为它的功能就是解析URL。

HttpContext类的Path属性原理

HttpContext的Path属性都是通过HttpRequest对象获得的。而事实上ASP.NET中的HttpRequest对象已经为我们提供一种直接通过URL构造的功能:

  1. var request = new HttpRequest(  
  2.     "",                                      /* filename */ 
  3.     "http://www.cnblogs.com/JeffreyZhao/",   /* url */ 
  4.     "hello=world");                          /* querystring */ 

估计ASP.NET开发团队也知道URL是个难办的问题,为我们预留了这样一个构造函数。这时的request对象会预填了大多数Path相关的属性:

  1. request  
  2. {System.Web.HttpRequest}  
  3.     AcceptTypes: null 
  4.     AnonymousID: null 
  5.     ApplicationPath: null 
  6.     AppRelativeCurrentExecutionFilePath: threw an exception of type 'System.NullReferenceException' 
  7.     Browser: null 
  8.     ClientCertificate: threw an exception of type 'System.NullReferenceException' 
  9.     ContentEncoding: threw an exception of type 'System.NullReferenceException' 
  10.     ContentLength: 0  
  11.     ContentType: "" 
  12.     Cookies: {System.Web.HttpCookieCollection}  
  13.     CurrentExecutionFilePath: "/JeffreyZhao/" 
  14.     FilePath: "/JeffreyZhao/" 
  15.     Files: {System.Web.HttpFileCollection}  
  16.     Filter: {System.Web.HttpInputStreamFilterSource}  
  17.     Form: {}  
  18.     Headers: {}  
  19.     HttpMethod: "GET" 
  20.     InputStream: {System.Web.HttpInputStream}  
  21.     IsAuthenticated: threw an exception of type 'System.NullReferenceException' 
  22.     IsLocal: false 
  23.     IsSecureConnection: false 
  24.     LogonUserIdentity: null 
  25.     Params: {hello=world}  
  26.     Path: "/JeffreyZhao/" 
  27.     PathInfo: "" 
  28.     PhysicalApplicationPath: threw an exception of type 'System.ArgumentNullException' 
  29.     PhysicalPath: "" 
  30.     QueryString: {hello=world}  
  31.     RawUrl: "/JeffreyZhao/?hello=world" 
  32.     RequestType: "GET" 
  33.     ServerVariables: {}  
  34.     TotalBytes: 0  
  35.     Url: {http://www.cnblogs.com/JeffreyZhao/}  
  36.     UrlReferrer: null 
  37.     UserAgent: null 
  38.     UserHostAddress: null 
  39.     UserHostName: null 
  40.     UserLanguages: null 

以上内容是从Visual Studio的Immediate Window中看到的,由此可以发现,其中大部分的Path属性已经准备好了,但是AppRelativeCurrentExecutionFilePath属性抛出异常(还有两个与本地磁盘路径有关的Path就忽略了),因为它需要特定的虚拟路径环境才能计算出来。通过.NET Reflector观察这个属性的实现,会发现其中牵涉到的内容不是一点两点,几乎不可能通过设置外部环境的方式来使其通过。因此,我们最终还是要通过Mock框架来进行设置——反正我们也需要设置HttpRequest的其它属性,不是吗?

  1. var realRequest = new HttpRequest(  
  2.     "",                                      /* filename */ 
  3.     "http://www.cnblogs.com/JeffreyZhao/",   /* url */ 
  4.     "hello=world");                          /* querystring */ 
  5. var mockRequest = new Mock<HttpRequestWrapper>(realRequest) { CallBase = true };  
  6. mockRequest  
  7.     .Setup(r => r.AppRelativeCurrentExecutionFilePath)  
  8.     .Returns("~" + realRequest.CurrentExecutionFilePath);  

这里还是使用Moq框架,而Mock的对象则是HttpRequestWrapper类型,而不是我们常用的HttpRequestBase类型。HttpRequestWrapper的特点便是可以“塞入”一个真正的HttpRequest对象,然后把所有成员都委托给这个HttpRequest对象。我们在构建一个Mock<HttpRequestWrapper>对象之后,还需要把CallBase属性设为true,这样便可以让Mock对象在默认情况下直接使用Wrapper的实现了。

有了Request,我们便可以构建一个HttpContext的Mock对象:

  1. var mockContext = new Mock<HttpContextBase>();  
  2. mockContext.Setup(c => c.Request).Returns(mockRequest.Object);  

但是,Moq框架有个限制,那就是如果您指定了这里的Request对象,再去通过HttpContext指定Request中的其他属性,就会把原来的HttpRequest对象给覆盖。也就是说,下面的代码会让我们对HttpRequest做的努力付之东流:

  1. mockContext.Setup(c => c.Request.Form).Returns(new NameValueCollection());  

这样您会发现,mockContext.Object.Request下除了Form外的其他属性都没有值了(或抛出异常,视您Mock时的Behavior是Loose还是Strict而定)。因此,如果我们希望进一步修改HttpRequest中属性的时候,只能直接使用那个Mock<HttpRequestWrapper>对象进行设置。我不清楚其他Mock框架的行为如何,如果您使用的也是Moq框架,可能就只得这么做了。

为了使用方便,我也在测试项目中准备了这样一个辅助方法:

  1. public static class MockHelper  
  2. {  
  3.     public static Mock<HttpContextBase> MockRequest(string url, out Mock<HttpRequestWrapper> mockRequest)  
  4.     {  
  5.         int index = url.IndexOf('?');  
  6.         string path = index >= 0 ? url.Substring(0, index) : url;  
  7.         string queryString = index >= 0 ? url.Substring(index + 1) : "";  
  8.  
  9.         var realRequest = new HttpRequest("", path, queryString);  
  10.         mockRequest = new Mock<HttpRequestWrapper>(realRequest) { CallBase = true };  
  11.         mockRequest  
  12.             .Setup(r => r.AppRelativeCurrentExecutionFilePath)  
  13.             .Returns("~" + realRequest.CurrentExecutionFilePath);  
  14.  
  15.         var mockContext = new Mock<HttpContextBase>();  
  16.         mockContext.Setup(c => c.Request).Returns(mockRequest.Object);  
  17.         return mockContext;  
  18.     }  
  19. }  
  20.  

于是我们就可以更方便地进行相关的单元测试。例如,我们“象征性”地测试一下ASP.NET Routing中内置的Route类型:

  1. [Fact]  
  2. public void URL_Capturing_and_Generation()  
  3. {  
  4.     // prepare route  
  5.     Route route = new Route("{controller}/{action}/{id}"null);  
  6.  
  7.     // Mock request  
  8.     string url = "http://www.cnblogs.com/Home/Index/5";  
  9.     Mock<HttpRequestWrapper> mockRequest;  
  10.     var mockContext = MockHelper.MockRequest(url, out mockRequest);  
  11.     mockContext.Setup(c => c.Response.Charset).Returns("utf-8"); // if you need  
  12.  
  13.     // test data capturing  
  14.     RouteData routeData = route.GetRouteData(mockContext.Object);  
  15.     Assert.Equal("Home", routeData.GetRequiredString("controller"));  
  16.     Assert.Equal("Index", routeData.GetRequiredString("action"));  
  17.     Assert.Equal("5", routeData.GetRequiredString("id"));  
  18.  
  19.     // test url generation  
  20.     var hash = new { controller = "Account", action = "List", id = 1};  
  21.     var values = new RouteValueDictionary(hash);  
  22.     var requestContext = new RequestContext(mockContext.Object, routeData);  
  23.     var pathData = route.GetVirtualPath(requestContext, values);  
  24.     Assert.Equal("Account/List/1", pathData.VirtualPath);  
  25. }  
  26.  

具体内容就叙述到这里,目前Path相关的问题应该已经不会给您造成太大问题了。

以上就是对HttpContext类的Path属性的问题解惑。本文来自老赵点滴:《在单元测试时指定HttpContext的各种Path》

【编辑推荐】

  1. 自定义的ControllerFactory:接口实现,支持Area
  2. ASP.NET Routing之“解析URL”功能详解
  3. 为ASP.NET MVC应用添加自定义路由
  4. 学习ASP.NET MVC路由的使用方法
  5. 浅析ASP.NET中的URL Rewrite
责任编辑:yangsai 来源: 老赵点滴
相关推荐

2009-07-24 11:33:12

MVC单元测试ASP.NET

2009-07-23 16:29:06

ASP.NET单元测试

2009-06-01 09:13:52

ASP.NET MVCMVC应用ASP.NET MVC

2021-04-26 14:25:39

ASP.NET Cor单元测试

2021-05-11 15:50:28

ASP.NET单元测试

2009-07-24 13:20:44

MVC框架ASP.NET

2009-07-31 12:43:59

ASP.NET MVC

2009-07-22 10:34:37

ActionInvokASP.NET MVC

2009-07-23 17:07:58

2009-07-28 13:17:09

EnableViewSASP.NET

2009-07-20 15:44:32

ASP.NET MVC

2009-07-22 09:11:02

Action方法ASP.NET MVC

2009-07-22 13:24:24

ASP.NET MVC

2009-07-20 10:53:59

ASP.NET MVC

2009-07-22 10:09:59

ASP.NET MVC

2009-07-23 14:31:20

ASP.NET MVC

2009-07-23 15:44:39

ASP.NET MVC

2009-08-13 11:44:25

ASP.NET中的多种

2011-09-22 10:58:56

ASP.NET

2009-12-01 09:30:34

ASP.NET MVC
点赞
收藏

51CTO技术栈公众号