Test Run

Test Automation for ASP.NET Web Apps with SSL

James McCaffrey

Code download available at:TestRun0408.exe(133 KB)

Contents

The Test Automation Program
Setting up a Test SSL Server
Further Work

If you're encrypting user data with Secure Sockets Layer (SSL) over HTTP and you want to test your Web applications programmatically you'll find that the techniques are not widely known. In this month's column I'll show you how to set up a test SSL server and write test automation that verifies the functionality of a simple but representative Web application.

The Microsoft® .NET environment provides powerful tools to test ASP.NET Web applications that use the SSL security mechanism. To illustrate their use, I will set up a test SSL server and demonstrate a short but powerful program that automatically tests a sample Web application that uses HTTPS. Although the individual techniques are elegant and documented, in conversations with many of my colleagues I discovered that the overall process of testing Web applications over SSL is relatively unfamiliar. The best way to show you where I'm headed is with two screen shots. Figure 1 shows the simplistic but representative ASP.NET Web application that I'll test.

Figure 1 An ASP.NET Web App

Figure 1** An ASP.NET Web App **

Notice that I'm using an SSL connection because I am sending sensitive credit card information over the Internet. (Notice the "https://" protocol in the address bar and the small lock icon in the status bar.)

Now, just imagine what it would be like to test this application manually. You would have to enter hundreds or even thousands of names, quantities, and credit card numbers into the Web page, visually examine each resulting confirmation code, manually check each code against a list of expected results to determine a pass or fail result, and then record the results in some form such as an Excel spreadsheet or text document. This would be time consuming, error prone, inefficient, and just plain boring.

A much better approach is to harness the powerful capabilities of the .NET Framework to write automation that programmatically sends the test data using SSL, then examines the response stream looking for an expected confirmation code. The Console application shown in Figure 2 does just that.

Figure 2 The Testing App

Figure 2** The Testing App **

As you can see, automated test case 001 corresponds to the manual test shown in Figure 1. The last name "Smith", quantity "3", and credit card number "1234 5678 9012" are encrypted and programmatically posted to the Web application using HTTP with SSL. The test program retrieves the HTTP response stream and searches it for "C3-57-ED-DA-8B". In this case, the expected confirmation code was found in the response stream so the test automation records a PASS result. In the three sections of this column that follow I will present and explain the test program that generated the output shown in Figure 2, demonstrate how to set up a test server that accepts SSL requests, and discuss how you can extend the techniques presented here to meet your own needs.

Before I move on to the test automation program, let's take a quick look at the sample Web application. As you can see in Figure 1, there are three TextBox controls; I accepted the Visual Studio® .NET default IDs of TextBox1, TextBox2, and TextBox3 to hold the user's last name, widget quantity, and credit card number, respectively. The application displays its message in the Label5 control. I'll need to know this information when I write the test automation. I'll also need to know how the order confirmation code is generated so that I can determine expected results for my test cases. Here is the heart of the code for the Web application under test:

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); }

To simulate the generation of a confirmation code, I just take the credit card number entered by the user, produce an MD5 hash of it, and grab the leftmost 14 characters of the hash. In an actual production system it is likely that you would want to generate a confirmation code in a more complicated manner. In such a case it might be tricky to determine the expected result. One strategy you should definitely not use, however, is to determine the expected result by calling the application under test. This would destroy the validity of your test because you'd just be checking to see if your test automation returns the same result as your application.

The Test Automation Program

The test automation program is surprisingly short. The entire code is presented in Figure 3. Although the techniques required to programmatically post data to an ASP.NET Web application are documented in the MSDN® Library, there are several tricks you need to watch out for.

Figure 3 The Test Automation Program

using System; using System.Web; // for HttpUtility class using System.Text; // for Encoding and StringBuilder classes using System.Net; // for HttpWebRequest class using System.IO; // for StreamReader class namespace Run { class Class1 { [STAThread] static void Main(string [] args) { Console.WriteLine("\nStarting Test Run\n"); string url = "https://localhost/LitwareOrder/Order.aspx"; string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA=="); string line; string[] tokens; StringBuilder data = new StringBuilder(); byte[] buffer; using(FileStream fs = new FileStream(args[0], FileMode.Open)) { StreamReader tc = new StreamReader(fs); while ((line = tc.ReadLine()) != null) { 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); buffer = Encoding.UTF8.GetBytes(data.ToString()); HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = buffer.Length; req.CookieContainer = new CookieContainer(); // enable cookies using (Stream reqst = req.GetRequestStream()) { reqst.Write(buffer, 0, buffer.Length); } Console.WriteLine("===================="); Console.WriteLine("\nTest case ID = " + tokens[0] ); Console.WriteLine("Posting " + tokens[1] + ", " + tokens[2] + "," + tokens[3] ); Console.WriteLine("To " + url); Console.WriteLine("Looking for " + tokens[4] ); using (HttpWebResponse res = (HttpWebResponse)req.GetResponse()) { string result; using(Stream resst = res.GetResponseStream()) { result = new StreamReader(resst).ReadToEnd(); } //Console.WriteLine(result); if (result.IndexOf(tokens[4]) >= 0) Console.WriteLine("PASS"); else Console.WriteLine("FAIL"); Console.WriteLine(""); } } // while loop Console.WriteLine("===================="); Console.WriteLine("Done"); } Console.ReadLine(); } // Main() } // class Class1 } // namespace Run

I decided to write a C# console application as my test program. It's usually a good idea to use the same language for your test automation as the system under test. However, the well-planned design and integration of the .NET environment means that you can safely use Visual Basic® .NET or any other .NET-compliant language. In general, console applications are the program type best suited for test automation. Although a slick test automation program with a nice user interface makes a great impression on your users, test automation is a tool, not a personal showcase, and console applications can be integrated into a build system more easily than a GUI application can.

The overall structure of the test automation is quite simple. I store test case data in a simple text file. Each line of data represents a single test case. Here is the test case file I used:

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

Information is separated by a colon character (:). I could have used any character as a delimiter, but it's important to avoid characters that appear in the actual test case data. The first field holds a test case ID, the second field holds the last name, the third field holds the quantity of widgets, the fourth field holds the credit card number, and the fifth field holds the expected confirmation code. If you don't want to use a text file, XML files and SQL tables make good test case storage alternatives.

The basic structure of my test automation is tied to the structure of my test case file. In pseudocode the plan looks like this:

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

I begin by declaring the namespaces I'll use. This lets me avoid fully qualifying each .NET class and object and also documents what functionality the test automation program will employ:

using System; using System.Web; using System.Text; using System.Net; using System.IO;

The System.Web namespace contains the HttpUtility class that I will use to convert special characters into escape sequences. Because a default Console Application does not reference System.Web.dll, which houses this class, I had to manually add a project reference to the file. The System.Text namespace has an Encoding class that I'll use to perform byte array manipulation. The System.Net namespace contains the HttpWebRequest class, which is the fundamental class I'll use to post data to the ASP.NET Web application. I use the System.IO namespace because I will use data streams for the HTTP response over SSL and because I'll need it to read test case data from a text file. Note that the using directive permits the use of types in a namespace such that you do not have to qualify the use of a type in that namespace.

Next, after displaying a brief start message to the command shell, I declare the key variables that the test automation uses:

string url = "https://localhost/LitwareOrder/Order.aspx"; string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA=="); string line; string[] tokens; StringBuilder data = new StringBuilder(); byte[] buffer; string proxy = null;

The purpose of most of these variables should be clear from their names but the viewstate variable may be new to you, so I will explain it shortly. Now, I open the test case file specified on the command line and read it line by line:

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 } }

There are many alternative ways to design the automation, but this simple structure has proven to be robust on several large projects. The next step is to parse each field of the test case data and build up a data string containing name-value pairs:

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);

I use the String.Split method to break up the test case line and store each field in the tokens array. The test case ID is stored in tokens[0], the user last name is stored in tokens[1], the widget quantity is stored in tokens[2], and the credit card number is stored in tokens[3]. For clarity, I could have created additional string variables with descriptive names like "caseID" and "lastName" and then copied values from the tokens array into them like so:

caseID = tokens[0]; lastName = tokens[1]; // etc.

But I wanted to keep the number of variables I used to a minimum. Traditional Web servers expect POST data in name-value pairs connected by an ampersand, like this:

lastName=Smith&quantity=3&creditCardNo=123456789012

However, ASP.NET extends this idea. In this example, there are five name-value pairs. The first pair, TextBox1=tokens[1], is what you might expect. It assigns the current test case last name value (stored in tokens[1]) to the control with id attribute "TextBox1". The second pair, TextBox2=tokens[2], and the third pair, TextBox3=tokens[3], should also make sense to you assigning the widget quantity and credit card number, respectively. The next pair, "Button1=clicked", is probably not what you expected if you have experience posting data to traditional ASP pages. Because Button1 is a server-side control I must post it to keep the ViewState value synchronized, as I'll explain in a moment. Assigning any value to it has no effect, so I could have written "Button1=" by itself. I like to use a value such as "clicked" anyway because it makes the code more readable. The fifth name-value pair is the __VIEWSTATE pair (note the two underscores); this is the real key to programmatically posting to ASP.NET servers.

What is this ViewState value? Even though HTTP is a stateless protocol—each Request-Response pair is an isolated transaction—ASP.NET works behind the scenes to create a stateful environment. One of the ways ASP.NET does this is through the use of an HTML hidden input named __VIEWSTATE. It is a Base64-encoded string value that represents the state of the page when last processed by the server. In this way, pages can maintain state by retaining values between successive calls. To correctly post information to an ASP.NET application, I must send the ViewState value to the server. Recall that I set the value of viewstate as:

string viewstate = HttpUtility.UrlEncode( "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA==");

Where did this value come from? The easiest way to get the initial ViewState value for a Web application is to just launch Microsoft Internet Explorer, get the page, and do a View | Source from the menu bar. It is important that you get the initial ViewState value because if you reload the page, the ViewState value will change and your programmatic post program will generate a server error. The raw ViewState value needs processing from the UrlEncode method. The UrlEncode method converts characters invalid in a URL into escaped sequences. For example, "=" characters are converted into %3D sequences.

After I have the ViewState value, I can construct the full data string that I will post to the Web application. Next I must convert this string into a byte array because the method that will later post the data requires the data to be stored as bytes:

buffer = Encoding.UTF8.GetBytes(data);

The GetBytes method is a member of the Encoding class in the System.Text namespace. In addition to the UTF8 property, there are ASCII, Unicode, and UTF7 properties, too. Now I instantiate an HttpWebRequest object and assign values to its properties:

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();

Notice that, following WebRequest's factory pattern, I use an explicit Create method rather than calling a constructor with the new keyword. I use the POST method because I am sending form data. I set the ContentType property to "application/x-www-form-urlencoded". This is a MIME type that you can think of as a magic string that tells the ASP.NET server to expect form data. I set the ContentLength property to the number of bytes of post data that I stored in the byte array buffer earlier.

I could have left out the Proxy property in this example if I were not sending data through a proxy server machine. The Boolean true parameter in the WebProxy constructor means to ignore the proxy for local addresses. Assigning a value to the CookieContainer property is required in order to have cookies returned in the Cookies property of the HttpWebRequest returned by GetResponse. Notice that I assign an empty CookieContainer object. This is one of the little details that caused me a lot of trouble when I was figuring out this technique.

Before I can post our request to the ASP.NET application, I have to add the post data to the Request object like this:

using (Stream reqst = req.GetRequestStream()) { reqst.Write(buffer, 0, buffer.Length); }

Next, after printing some information about what I am posting, I retrieve the resulting response stream from the server:

using(HttpWebResponse res = (HttpWebResponse)req.GetResponse()) { string result; using(Stream resst = res.GetResponseStream()) { result = new StreamReader(resst).ReadToEnd(); } //Console.WriteLine(result); }

You might have expected, as I did, that I would explicitly send the request with a statement like req.Send(data). However using HttpWebRequest.GetRequestStream actually opens the connection to the server, and HttpWebRequest.GetResponse retrieves the HttpWebResponse object that represents the server's response. (If GetRequestStream is not used, GetResponse actually makes the connection to the server, as well.) I grab the entire response stream using ReadToEnd and save it into a string variable named "result". You can also read the response line-by-line using the ReadLine method. Notice I commented out a statement that would display the entire response stream to the command shell. If you are new to this kind of programming, you will find it instructive to uncomment that line so you can see the entire response stream.

Finally, I examine the response stream to see if the expected confirmation code (stored in tokens[4]) is in the stream:

if (result.IndexOf(tokens[4]) >= 0) Console.WriteLine("PASS"); else Console.WriteLine("FAIL");

If I find the expected value, I log a PASS result to the shell. Of course, you could write test case results to a text file, XML file, or SQL table, if you prefer.

Setting up a Test SSL Server

Until recently, it was quite a chore to set up a test Web server with SSL enabled. You can purchase a "real" SSL certificate from one of several providers, but that takes time and money. Another option is to generate a self-signed SSL certificate using the makecert.exe utility that is part of the .NET Framework Tools, then install it onto your Web server. But now there is a much simpler way.

The IIS 6.0 Resource Kit (available for download from Windows Deployment and Resource Kits) contains several valuable tools including one—selfssl.exe—which makes it very easy to create and install a self-signed SSL certificate for testing purposes. The screenshot in Figure 4 shows exactly how I did this.

Figure 4 Create a Self-Signed Certificate

Figure 4 Create a Self-Signed Certificate

The key is to use the /T switch so that the local browser will trust the certificate and also to use the /N switch to specify "localhost" as the Common name. Amazingly, this is all you need to test with HTTPS directly on the Web server. If you want to test HTTP with SSL from a remote client machine, the first time you manually browse to the test server you will get a Security Alert dialog asking if you want to proceed. If you click on the View Certificate button, then click on the Install Certificate button, you will enter a wizard. If you accept all the defaults in the wizard, then after you finish installing the certificate the client will be able to access the test server without the warning dialog and your test automation will run from the client.

Although the selfssl.exe tool is part of the IIS 6.0 Resource Kit and does not explicitly support earlier versions of IIS, my colleagues and I have successfully experimented with it on IIS 5.0. I have also used the makecert.exe tool to generate a self-signed x.509 certificate that can be used for testing. The MSDN Library has instructions for the makecert.exe tool at Certificate Creation Tool, but using the selfssl.exe tool is easier.

After you are finished testing with your self-signed SSL certificate you will want to remove it to prevent possible interaction effects on your test server. The easiest way to remove the certificate is by using the Microsoft Management Console (MMC). Launch MMC and add the Certificates snap-in for a Computer Account to manage the Local Computer. Now you should expand the Certificates store and then expand the Personal folder. After selecting the Certificates folder, your self-signed certificate will be displayed and you can delete it.

Further Work

There are several ways you can extend the techniques presented in this month's column. I hardcoded some information (for example, the test URL) which you'll want to pull out, and you'll probably find it useful to parameterize to make your test system more flexible. An interesting extension would be to programmatically determine the initial ViewState value. Recall that I launched the browser manually then viewed the source to determine the initial ViewState value, then hardcoded that value into the automation. This works, but every time there is a change to the Web application code base, you'll have to get the new ViewState value. A better approach is to use the WebClient class from the System.Net namespace to programmatically request the initial Web application page, parse the response stream for the __VIEWSTATE value, and then assign that value to the viewstate variable. This adds an additional request/response round-trip to each test case call, but it greatly increases the flexibility of your test automation.

The technique presented in this column nicely complements testing that you can perform with the Microsoft Application Center Test (ACT) tool. ACT is designed to stress test Web servers and analyze the performance of Web apps so you can deal with scalability problems. However, ACT was not designed to handle functional verification like the technique shown here was designed to do.

In this column I used the HttpWebRequest class to programmatically post data to the ASP.NET Web application under test, but there are several alternatives you can use. At a lower level of abstraction you can post data using the Sockets class in the System.Net.Sockets namespace. At a slightly higher level of abstraction you can use the WebClient class in the System.Net namespace. All three techniques work fine, but most of my colleagues and I prefer the HttpWebRequest class. However, you may find that it is matter of personal preference and scenario.

Send your questions and comments for James to  testrun@microsoft.com.

James McCaffrey works for Volt Information Sciences Inc., where he manages technical training for software engineers working at Microsoft. He has worked on several Microsoft products including Internet Explorer and MSN Search. James can be reached at jmccaffrey@volt.com or v-jammc@microsoft.com.