走进单元测试(3):消灭HttpContext的依赖,兼谈单元测试的设计辅助性

前篇提到过由于我们已经有了一个现成的平台,现在要对其进行单元测试的补完。而在这个过程中,就出现了HttpContext这类东西,其依附于一个host环境,对单元测试的自动化是一个很大的阻碍。

对于HttpContext,如果没有一个web托管环境,其中的Request和Response等只读属性根本就无法造出来。而如果要搭建一个web托管环境,不仅为测试带来了干扰(因为要确定是否是托管环境的问题),而且给测试的自动化带来了不方便。那么怎么去解决这个问题呢?

在MSDN中我们可以查到一个叫SimpleWorkerRequest的东西,这个东西的提供了一个简单的System.Web.HttpWorkerRequest的实现,使得我们可以在IIS之外托管ASP.NET应用程序。而当我们使用reflector来查看这个东西的源码的时候,发现其中的一些方法很有趣:

SimpleWorkerRequest
 1 [ComVisible(false), AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]
 2 public class SimpleWorkerRequest : HttpWorkerRequest
 3 {
 4     //...
 5 
 6     public override string GetHttpVerbName()
 7     {
 8         return "GET";
 9     }
10 
11     public override string GetHttpVersion()
12     {
13         return "HTTP/1.0";
14     }
15 
16     public override string GetLocalAddress()
17     {
18         return "127.0.0.1";
19     }
20 
21     public override int GetLocalPort()
22     {
23         return 80;
24     } 
25 
26     //...
27 }

这果然是一个简单的实现啊,把IP地址,Http版本,端口等全部硬编码了。

但对我们的需求来说,其实也非常的简单,既然他硬编码了,那我们再派生一下,把这些方法override一下不就可以了:

TestWorkerRequest
 1     /// <summary>
 2     /// Provides a simple implementation of the System.Web.HttpWorkerRequest abstract class that can be used to host ASP.NET applications outside an Internet Information Services (IIS) application. 
 3     /// This class can be used for unit test which needs a web host.
 4     /// </summary>
 5     public class TestWorkerRequest : SimpleWorkerRequest
 6     {
 7         private readonly string hostName = "";
 8 
 9         /// <summary>
10         /// Initializes a new instance of the <see cref="T:System.Web.Hosting.SimpleWorkerRequest"/> class when the target application domain has been created using the <see cref="M:System.Web.Hosting.ApplicationHost.CreateApplicationHost(System.Type,System.String,System.String)"/> method.
11         /// </summary>
12         /// <param name="page">The page to be requested (or the virtual path to the page, relative to the application directory).</param>
13         /// <param name="query">The text of the query string.</param>
14         /// <param name="output"><see cref="T:System.IO.TextWriter"/> that captures output from the response</param>
15         /// <param name="hostName">The host name that will be requested.</param>
16         public TestWorkerRequest(string page, string query, TextWriter output, string hostName) 
17             : base(page, query, output)
18         {
19             this.hostName = hostName;
20         }
21 
22         /// <summary>
23         /// Initializes a new instance of the <see cref="T:System.Web.Hosting.SimpleWorkerRequest"/> class for use in an arbitrary application domain, when the user code creates an <see cref="T:System.Web.HttpContext"/> (passing the SimpleWorkerRequest as an argument to the HttpContext constructor).
24         /// </summary>
25         /// <param name="appVirtualDir">The virtual path to the application directory; for example, "/app".</param>
26         /// <param name="appPhysicalDir">The physical path to the application directory; for example, "c:\app".</param>
27         /// <param name="page">The virtual path for the request (relative to the application directory).</param>
28         /// <param name="query">The text of the query string.</param>
29         /// <param name="output"><see cref="T:System.IO.TextWriter"/> that captures the output from the response.</param>
30         /// <param name="hostName">The host name that will be requested.</param>
31         /// <exception cref="T:System.Web.HttpException">The <paramref name="appVirtualDir"/> parameter cannot be overridden in this context.
32         /// </exception>
33         public TestWorkerRequest(string appVirtualDir, string appPhysicalDir, string page, string query, TextWriter output, string hostName) 
34             : base(appVirtualDir, appPhysicalDir, page, query, output)
35         {
36             this.hostName = hostName;
37         }
38 
39         /// <summary>
40         /// Returns the server IP address of the interface on which the request was received.
41         /// </summary>
42         /// <returns>
43         /// The server IP address of the interface on which the request was received.
44         /// </returns>
45         public override string GetLocalAddress()
46         {
47             return hostName;
48         }
49     }

那么使用如下代码便可以模拟出一个HttpContext了,这样依赖就不存在了。

代码
            //设置Cookie环境
             Thread.GetDomain().SetData(".appPath"@"D:\Test");
            Thread.GetDomain().SetData(
".appVPath""/");
            TextWriter tw 
= new StringWriter();
            
string address = "http://www.sina.com.cn/";
            HttpWorkerRequest wr 
= new MyWorkerRequest("login.aspx""", tw, address);
            HttpContext.Current 
= new HttpContext(wr);

其实说这个事情,本身并不是为了这个技巧,而是想借这个例子说明怎么去考虑层的职责。比如对HttpContext这个东西,因为你知道你现在设计的是Web程序,你直接使用了这个。但如果有一天同样的业务,让你做一个WinForm呢?HttpContext该怎么办?所以,这样一分析就知道HttpContext这个东西肯定不属于逻辑层。

而如果你对逻辑层做单元测试的话,那么你必定会遇到上述问题。而一旦遇到这种问题,应该就说明了你的设计思路有问题,因为从逻辑本身来说,实现一个测试,我不应该需要借助任何的模拟。至于Mock这个东西,留给以后的篇幅吧。

原文地址:https://www.cnblogs.com/gamix/p/about_httpcontext_in_unittest.html