分类: C/C++
2008-04-02 14:31:55
如果你正在HTTP上使用安全套接字层(SSL)来加密用户数据,并且想通过编程来测试你的Web应用,你会发现此技术并非广为人知。在本月的栏目中,我将示范如何建立一个 SSL 测试服务器,然后编写测试自动化代码,并通过一个简单而又具有代表性的 Web 应用来验证。
注意我使用的是 SSL 连接,因为我要在 Internet 上传送敏感的信用卡信息(注意是"https://"协议,并且在状态栏有一个小锁 图标)。
正像你看到的,自动化测试案例的基本做法与 Figure 1 中所示的手动测试是一样的。用户名称是"Smith",物品数量是"3",信用卡号是"1234 5678 9012", 通过基于 SSL 的 HTTP 加密后被提交到Web应用,测试程序获取 HTTP 响应流,并搜索响应流中的“C3-57-ED-DA-8B”,这时,在该响应流中找到期望的确认码,所以测试 自动化程序记录下“PASS”结果。在本栏目后面的三个章节中,我将讲解产生如 Figure 2 所示输出的测试程序。演示如何建立一个接受 SSL 请求的测试服务器,并讨论如何扩展本文呈现的技术来满足你自己的需要。 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个字符。在实际的生产系统中,你可能会用更为复杂的方式来产生确认码。在这种情况下确定预期的结果可能会更具技巧性。不过有一点要特别注意,你不能通过调用被测试的程序来确定预期结果,因为这将破坏测试的有效性,因为你本来就是要检查 测试自动化程序返回的结果和被测程序返回的结果是否一致。 测试自动化程序 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 = ""; 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服务器 最关键的地方。 string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA=="); 这个值是从何而来的呢?获得某个Web应用程序 ViewState 初始值的最简单的方法是:只要启动 IE 浏览器得到该页面,然后用菜单栏“查看|源文件”打开源文件 便可以检索到。获得 ViewState 的初始值非常重要,因为如果你重新 加载这个页面,ViewState 的值将会变化,你 所编写的提交数据的程序将产生一个服务器错误。原始的 ViewState 值需要使用 UrlEncode 方法处理,UrlEncode方法将 URL 中的无效字符转化为转义字符序列,比如”=”可以转化为%3D。 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 属性设置为 所提交数据的字节数,这个数据先前已保存在字节数组缓冲中。 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 方法一行一行地读取响应。注意我 注释掉了一条在命令外壳显示整个响应流的语句,如果你这方面编程的新手,去掉这个注释,以便看到整个响应流,这对你来说是有所裨益的。 if (result.IndexOf(tokens[4]) >= 0) Console.WriteLine("PASS"); else Console.WriteLine("FAIL"); 如果我发现预期的结果,便向外壳记录一个 PASS 结果,当然,如果你愿意,也可以将测试案例结果写到一个文本文件,XML文件或者 SQL 表 中。 设置SSL测试服务器 设置启用 SSL 的测试 Web 服务器 到现在都是一件令人繁琐的事情。你可以从几个供应商之一处购买一个“真实”的 SSL 证书,不过这需要花费一定的时间和金钱。另一个方法是使用makecert.exe 实用程序来产生一个自签名的 证书。它是.NET框架工具 的一部分,然后将它安装到你的 Web服务器 上。但现在我有更简单的方法。 进一步的工作 |
作者简介: James McCaffrey 供职于 Volt Information Sciences Inc. 在那里他负责微软公司软件工程师的技术培训工作。他曾参与过微软的几个产品,包括:IE 和 MSN Search。可以通过 jmccaffrey@volt.com 或 v-jammc@microsoft.com 与 James 联系。 |