走进单元测试(3):消灭HttpContext的依赖,兼谈单元测试的设计辅助性
前篇提到过由于我们已经有了一个现成的平台,现在要对其进行单元测试的补完。而在这个过程中,就出现了HttpContext这类东西,其依附于一个host环境,对单元测试的自动化是一个很大的阻碍。
对于HttpContext,如果没有一个web托管环境,其中的Request和Response等只读属性根本就无法造出来。而如果要搭建一个web托管环境,不仅为测试带来了干扰(因为要确定是否是托管环境的问题),而且给测试的自动化带来了不方便。那么怎么去解决这个问题呢?
在MSDN中我们可以查到一个叫SimpleWorkerRequest的东西,这个东西的提供了一个简单的System.Web.HttpWorkerRequest的实现,使得我们可以在IIS之外托管ASP.NET应用程序。而当我们使用reflector来查看这个东西的源码的时候,发现其中的一些方法很有趣:
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一下不就可以了:
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">A <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">A <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了,这样依赖就不存在了。
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这个东西,留给以后的篇幅吧。