Microsoft .NET环境提供了强有力的测试工具来测试采用SSL安全机制的ASP.NET Web应用程序。为了说明它们的使用方法,我将建立一个SSL服务器,并通过一个短小精悍的程序来示范如何自动测试基于HTTPS的Web例子应用程序。虽然 在这方面已有个别技术非常经典并且有很好的文章介绍,但在与我的许多同事交流中,我发现对测试 SSL Web 应用的整体过程的把握相对来说并不是很到位。用两幅屏幕截图最能说明问题。Figure 1展示了一个简单但是很有代表性的ASP.NET的Web应用。
Figure 1 一个 ASP.NET Web 应用
注意我使用的是 SSL 连接,因为我要在 Internet 上传送敏感的信用卡信息(注意是"https://"协议,并且在状态栏有一个小锁 图标)。
现在,让我们想象一下用手工方式是如何测试这个应用程序的。我们必须在Web页上输入成百甚至上千的用户名,数量以及信用卡号码,检查每一个确认码,针对预期的结果检查每行代码以确定结果是否正确,然后将这些结果记录在一些表格中,比如 Excel 电子表格或者文本文件中。整个过程耗时、低效、繁琐并且容易出错。
一个更好的方法是利用.NET框架的强大能力编写自动化测试例程,在程序中用 SSL 发送测试数据,然后针对预期的确认码来检查响应流,Figure 2 是一个控制台应用程序,它演示了上述的思路。
Figure 2 测试应用
正像你看到的,自动化测试案例的基本做法与 Figure 1 中所示的手动测试是一样的。用户名称是"Smith",物品数量是"3",信用卡号是"1234 5678 9012", 通过基于 SSL 的 HTTP 加密后被提交到Web应用,测试程序获取 HTTP 响应流,并搜索响应流中的“C3-57-ED-DA-8B”,这时,在该响应流中找到期望的确认码,所以测试 自动化程序记录下“PASS”结果。在本栏目后面的三个章节中,我将讲解产生如 Figure 2 所示输出的测试程序。演示如何建立一个接受 SSL 请求的测试服务器,并讨论如何扩展本文呈现的技术来满足你自己的需要。
在我讲解如何编写测试自动化程序之前,让我们首先快速回顾一下本文的例子Web应用程序。正如你在 Figure 1 看到的一样,有三个 TextBox 控件,我使用 Visual Studio .NET 缺省的ID:TextBox1、TextBox2、TextBox3 来命名它们。它们分别对应着用户名称、物品数量以及信用卡帐号。 Label5 控件 用于显示应用程序信息。当我编写测试自动化程序时,我必须要知道这些信息,此外还需要知道订单确认码的产生方式,以便我能确定我的测试案例期望的结果。以下是用于测试此Web应用程序的核心代码:
if (TextBox3.Text.Length == 0) Label5.Text = "Please enter credit card number"; else { byte[] input = Encoding.Unicode.GetBytes(TextBox3.Text); byte[] hashed; using(MD5 m = new MD5CryptoServiceProvider()) { hashed = m.ComputeHash(input); } Label5.Text = "Thank you. Your confirmation code is " + BitConverter.ToString(hashed).Substring(0,14); }
为了模拟确认码的生成,我只利用了用户输入的信用卡号,用它产生一个MD5散列,然后截取散列值最左边的14个字符。在实际的生产系统中,你可能会用更为复杂的方式来产生确认码。在这种情况下确定预期的结果可能会更具技巧性。不过有一点要特别注意,你不能通过调用被测试的程序来确定预期结果,因为这将破坏测试的有效性,因为你本来就是要检查 测试自动化程序返回的结果和被测程序返回的结果是否一致。
测试自动化程序
这个测试自动化程序出奇的短小。其全部代码如 Figure 3 所示。尽管通过编程将数据提交给某个ASP.NET Web 应用程序的技术在 MSDN 库中已有文档描述,但是其中有几个技巧需要特别关注 。
我决定编写一个C#控制台程序作为我的测试程序。使用和被测程序一样的语言开发测试自动化程序通常是个好主意。不管怎么说,规划良好的设计并与.NET环境集成意味着你能安全地使用Visual Basic .NET或其它任何与.NET兼容的语言。一般来说,控制台程序类型最适合作为 测试自动化程序。虽然测试程序具备漂亮的用户界面能给用户留下深刻印象,但自动化测试程序是一个工具,而不是个人秀,此外,控制台程序也比GUI程序更容易集成到构建系统中。
测试自动化程序的整个结构相当简单。我将测试案例的数据保存在一个简单的文本文件中,每一行数据表示单个测试案例,以下是该测试案例文件的内容:
001:Smith:3:1234 5678 9012:C3-57-ED-DA-8B 002:Baker:2:1111 2222 3333:CE-81-8C-2F-94 003:Gates:9:9999 9999 9999:95-D6-05-31-8A
信息之间使用冒号(:)进行分隔。我也可以使用任何字符作为分隔符,但在实际的测试案例中避免出现含义模糊的字符很重要。第一个字段是测试案例编号,第二个字 是用户名称,第三个字段是数量,第四个字段是信用卡号码,第五个字段是预期的确认码。如果你不想使用文本文件,那么XML文件或 SQL 表 都是很好的可选方案。
我的测试自动化程序的基本结构与我的测试案例数据文件是相关在一起的。使用伪代码表示如下:
loop read a test case line parse out test case data build up data to post to application convert post data to a byte array post the data retrieve the response stream if response stream contains expected confirmation code log "pass" result else log "fail" result end loop我首先声明要用到的命名空间,这样可以避免用到每个.NET类和对象时都得写全称限定名。同时测试自动化程序将要涉及哪些类库功能也一目了然。
using System; using System.Web; using System.Text; using System.Net; using System.IO;System.Web 命名空间包含了 HttpUtility 类,这个类可以将一些特殊字符转换为转义字符序列,因为缺省的控制台程序并不引用它的所在程序集,即 System.Web.dll,我们必须手动地添加对它的引用。System.Text 命名空间包含了一个Encoding 类,我要用它来处理字节数组 (Byte Array)。System.Net 命名空间包含了 HttpWebRequest类, 它是将数据提交到 ASP.NET Web 应用 的基础类。使用 System.IO 命名空间 是因为我要用数据流处理基于 SSL 的 HTTP 的响应,此外我还需要用它从文本文件中读取测试案例数据。注意:using 指令字 允许你在使用某个命名空间中的类型时,不必用长长的限定名。
接下来,在命令外壳中显示一段简单的启动信息后,声明测试自动化 程序要用到的一些关键变量:
string url = "https://localhost/LitwareOrder/Order.aspx"; string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA=="); string line; string[] tokens; StringBuilder data = new StringBuidler(); byte[] buffer; string proxy = null;
上面大多数变量的目的从其命名一目了然,只有 viewstate 是个新变量,所以我会对之作简要解释。现在我打开测试案例文件,并且一行一行地读取:
using(FileStream fs = new FileStream(args[0], FileMode.Open)) { StreamReader tc = new StreamReader(fs); while ((line = tc.ReadLine()) != null) { // parse line, post data, get response // determine pass or fail, log result } }
虽然有很多可选方法来设计此自动化过程,但是 上述这个简单的结构已经在几个大型项目中被证明是健壮的。下一步是解析测试案例中数据的每个字段,并且构建一个包含“名称-值”对 的字符串。
tokens = line.Split('':''); data.Length = 0; data.Append("TextBox1=" + tokens[1]); // Last name data.Append("&TextBox2=" + tokens[2]); // Quantity data.Append("&TextBox3=" + tokens[3]); // Credit card number data.Append("&Button1=clicked"); data.Append("&__VIEWSTATE=" + viewstate);
我使用String.Split方法将测试 案例数据行分开,并且将每个字段保存到tokens数组中, 测试案例的ID保存到tokens[0]中,用户名称保存到tokens[1]中,物品数量保存到tokens[2]中,信用卡号保存到tokens[3]中。为了清晰起见,也可以将这些数值复制到额外的 具有描述性的字符串变量中,如:"caseID","lastName"等,如下所示:
caseID = tokens[0]; lastName = tokens[1]; // etc.
但是我想让所使用的变量数为最少,传统的Web服务器 一般都用“名称-值”对来 提交(POST)数据,多个数据之间用“&”符号分开,如下:
lastName=Smith&quantity=3&creditCardNo=123456789012
但是,ASP.NET扩展了这种做法,在这个例子中 ,有五个"名称-值"对,第一对,你可能希望是:TextBox1=tokens[1],它将当前测试 案例的用户名称(保存在tokens[1]中)赋值给ID属性为"TextBox1"的控件。第二对是TextBox2=tokens[2],第三对是TextBox3=tokens[3],它们分别将物品数量和信用卡号赋值给对应的控件。下一对是"Button1=clicked", 如果你用过传统的ASP页面提交数据,那么它可能和你想象 中的不一样。因为在ASP.NET中,Button1是一个服务器端控件,我必须同步的保持 ViewState 值 ,稍后会对此加以解释。对它赋任何值都是没有作用的,所以我索性就用"Button1="这样的代码。我更喜欢使用诸如"clicked"的形式,因为这样可读性更高。第五对是__VIEWSTATE(注意前面的 两个下划线),这是编程提交数据给ASP.NET服务器 最关键的地方。
SiewState的值是什么? 虽然 HTTP 一个无状态的协议——每一个请求——响应都是相互隔离的事务——ASP.NET则在幕后处理以创建一个支持状态的环境。它使用的一种方法是 用HTML隐藏 一个名为 _VIEWSTATE 的输入控件。这是一个经过编码的 Base64 字符串,用来表示服务器做最后一次处理时的页面状态,使用这种方法,通过保留每次调用前后的值来维护页面的状态,为了正确的向ASP.NET Web应用程序提交数据,必须将ViewState的值发送到服务器。回顾一下我是如何设置这个值的:
string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA==");
这个值是从何而来的呢?获得某个Web应用程序 ViewState 初始值的最简单的方法是:只要启动 IE 浏览器得到该页面,然后用菜单栏“查看|源文件”打开源文件 便可以检索到。获得 ViewState 的初始值非常重要,因为如果你重新 加载这个页面,ViewState 的值将会变化,你 所编写的提交数据的程序将产生一个服务器错误。原始的 ViewState 值需要使用 UrlEncode 方法处理,UrlEncode方法将 URL 中的无效字符转化为转义字符序列,比如”=”可以转化为%3D。
一旦有了 ViewState 值之后, 便可以构造出所有要提交到 Web 应用的数据字符串。下一步是将这个字符串转化为字节数组,因为稍后提交该数据的方法需要以字节形式存储的数据:
buffer = Encoding.UTF8.GetBytes(data);
GetBytes 方法是 System.Text 命名空间中的 Encoding 类的一个成员函数。 除了 UTF 属性以外,还有 ASCII、Unicode、UTF7 等属性。现在我们实例化一个 HttpWebRequest 对象,并给其属性赋值:
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = buffer.Length; req.Proxy = new WebProxy(proxy, true); req.CookieContainer = new CookieContainer();
注意,WebRequest 之工厂模式,这里我显式地调用 Create 方法, 而不是用 new 关键字调用构造函数。我用 POST 方法,因为我发送表单数据。我将 ContentType 属性设置为"application/x-www-form-urlencoded"。这是个 MIME 类型 ,你可以把它看作是一个神奇的字符串,它告诉 ASP.NET服务器 接收表单数据。将 ContentLength 属性设置为 所提交数据的字节数,这个数据先前已保存在字节数组缓冲中。
在例子中,如果不通过代理服务器发送数据,可以不用理会 Proxy 属性。在 WebProxy 构造函数中的布尔型参数是true,意思是本地地址则忽略代理。为了获取由 Cookies 属性返回的 cookies,必须给 CookieContainer 属性赋值,这里 HttpWebRequest 由 GetResponse 返回。注意我 为 CookieContainer 对象赋了一个空值。我在领会这个技术时,这是我碰到的许多导致麻烦的细节问题之一。
在我们将数据提交给 ASP.NET 应用之前, 必须将要提交的数据添加到 Request 对象。就像下面这样:
using (Stream reqst = req.GetRequestStream()) { reqst.Write(buffer, 0, buffer.Length); }
下一步,在输出了一些我所提交的信息后,我收到从服务器返回的结果响应数据流。
using(HttpWebResponse res = (HttpWebResponse)req.GetResponse()) { string result; using(Stream resst = res.GetResponseStream()) { result = new StreamReader(resst).ReadToEnd(); } //Console.WriteLine(result); }
你可能和我一样期望使用类似于 req.Send(data) 这样的语句来发送数据,但是使用 HttpWebRequest.GetRequestStream 实际上是打开和服务器的连接,并用 HttpWebRequest.GetResponse 获取 HttpWebResponse 对象,它 表示服务器端的响应。(如果不使用 GetRequestStream,实际上 GetResponse 也会建立到服务器的连接)。我用 ReadToEnd 取得整个将响应流并保存到一个叫做“result”的字符串变量中。你 也可以用 ReadLine 方法一行一行地读取响应。注意我 注释掉了一条在命令外壳显示整个响应流的语句,如果你这方面编程的新手,去掉这个注释,以便看到整个响应流,这对你来说是有所裨益的。
最后,我检查响应数据流以便确认预期的验证码是否包含在其中(它保存在 tokens[4]中)。
if (result.IndexOf(tokens[4]) >= 0) Console.WriteLine("PASS"); else Console.WriteLine("FAIL");
如果我发现预期的结果,便向外壳记录一个 PASS 结果,当然,如果你愿意,也可以将测试案例结果写到一个文本文件,XML文件或者 SQL 表 中。
设置SSL测试服务器
设置启用 SSL 的测试 Web 服务器 到现在都是一件令人繁琐的事情。你可以从几个供应商之一处购买一个“真实”的 SSL 证书,不过这需要花费一定的时间和金钱。另一个方法是使用makecert.exe 实用程序来产生一个自签名的 证书。它是.NET框架工具 的一部分,然后将它安装到你的 Web服务器 上。但现在我有更简单的方法。
IIS6.0的资源工具包(可以从Windows部署和资源工具包中下载)包含多个有用的工具,其中就有selfssl.exe,它使得创建和安装用于测试目的的自签名SSL证书十分容易。Figure 4 的截图详细示范了此工具的使用方法。
Figure 4
关键是使用/T开关,以便本地浏览器信任此证书,同时还要使用/N开关指定Localhost作为公共名字。令人惊讶是你只需要做这些便可在 Web 服务器上直接测试HTTPS。如果你想从远程客户机上测试基于SSL的HTTP,那么第一次手动浏览测试服务器时,会弹出一个安全警告对话框,询问你是否继续。如果你单击“查看证书”按钮,然后单击“安装证书”按钮,你会进入一个向导。如果你在向导中接受所有的默认选项。完成证书安装之后,客户端便能访问测试服务器,不会再出现警告对话框,并且测试自动化程序将从客户端运行。
尽管selfssl.exe工具是IIS6.0资源工具包的一部分,并且没有明确是否支持早期的IIS版本,我和我的同事已经成功地在 IIS 5.0 上进行了实验,我还使用了makecert.exe工具来产生能被用于测试的x.509证书。MSDN库的Certificate Creation Tool 中有对makecert.exe工具的介绍,相对来说selfssl.exe 工具的使用要容易一些。
当你用自签名的 SSL 证书完成测试之后,你会想从测试服务器中删除证书,以免可能对测试服务器产生的交互影响。删除证书最容易的方法是使用微软管理控制台(MMC)。执行MMC,并为管理本地计算机的计算机帐户添加一个证书管理单元。现在,你应该展开“证书”,然后“个人”,选择“证书”文件夹后,你的自签名证书会显示出来,这时删除它即可。
进一步的工作
你可以有许多种方式来扩展本月专栏中提供的技术。有些信息我采用的是硬编码的方式(比如:测试URL),这些可能并不适合你,你可能会发现对它们进行参数化可以使得你的测试系统更加灵活。另外一个有趣的扩展是通过编程确定ViewState的初始值。回想我在范例中是手工启动浏览器,然后查看源代码得到ViewState的初始值,然后通过硬编码写到测试程序的。虽然按照这种方式能行得通,但每一次Web应用程序的核心代码一有修改,你都不得不从新获取ViewState。一个更好的解决方法是使用System.Net命名空间的WebClient 类,通过编程来请求初始的 Web 应用程序页面,解析响应流来抽取出__VIEWSTATE 的值,然后将它赋给 viewstate 变量。这种方法虽然在调用每一个测试案例时,都增加了一次额外的请求/响应往返开销,但是它大大增加了你的测试自动化程序的灵活性。
本专栏所展示的技术是对用 Microsoft Application Center Test (ACT)工具进行测试的一个很好的补充,ACT 被设计用来对Web服务器进行压力测试,并分析Web应用的性能,以便解决伸缩性问题。但是,ACT 不是被用来处理功能验证的,而这里讲述的技术主要用于功能验证。
本专栏中,我使用了HttpWebRequest类通过编程向拟要测试的ASP.NET Web应用程序提交数据,几种可替代的方法都可以使用。在较低层次,你可以使用System.Net.Sockets命名空间中的Sockets类,在较高层次,你可以使用System.Net命名空间的WebClient类。这三种技术都可以很好的工作,但是我和我的大多数同事都喜欢使用 HttpWebRequest类,不管怎样,你会发现这是个人爱好以及根据具体情况选择不同的实现方式而已。