Test Run

Request/Response Testing with Windows PowerShell

Dr. James McCaffrey

Code download available at:TestRun2008_05.exe(152 KB)

Contents

The Application under Test
Fundamental ASP.NET Request/Response Testing
Inside the Helper Function
Extracting ViewState and EventValidation
Working with the ViewState and EventValidation Values
Extending the Request/Response Automation

The most fundamental form of Web testing is HTTP request/response testing. This involves programmatically sending an HTTP request to the Web application, fetching the HTTP response, and examining the response for an expected value.

When you need to perform request/response testing for ASP.NET applications, there are a number of approaches from which you can choose. You can write a C# or Visual Basic® console application. You can write JavaScript or Perl scripts. If you're a glutton for punishment you can write a C/C++ program. And nowadays, you can use Windows PowerShell™.

I've used many of these techniques, and Windows PowerShell has become my preferred method for lightweight HTTP request/response testing. I can simply write a small Windows PowerShell script that programmatically posts information to an ASP.NET application on a Web server. The script can then retrieve the HTTP response and examine it for an expected value to determine a pass/fail result.

In this month's column, I will show you how to use Windows PowerShell to perform request/response testing for an ASP.NET application. I'll assume you have at least some basic familiarity with ASP.NET and with Windows PowerShell. But even if these topics are new to you, you should still be able to follow this discussion without too much trouble.

Figure 1 shows the simple but representative ASP.NET Web application named MiniCalc. As you can see, the MiniCalc application accepts two integers, computes either a sum or a product, and displays the result out to four decimals places. Real Web applications are significantly more complex than this simple sample app, but the techniques I'm discussing will easily scale to more complex applications.

Figure 1 Simple ASP.NET Web Application under Test

Figure 1** Simple ASP.NET Web Application under Test **

Figure 2 shows my Windows PowerShell test script in action. For simplicity, I have hard-coded all of the information regarding the application being tested. I will explain how you can parameterize the script later. The script first echoes the target URL of https://localhost/MiniCalc/Default.aspx and then performs a probing request for the Web app's initial ViewState and EventValidation values. These are key values, which I'll explain shortly.

Figure 2 Windows PowerShell Test Script in Action

Figure 2** Windows PowerShell Test Script in Action **

Next my script programmatically posts information that simulates a user typing 5 into TextBox1, 3 into TextBox2, selecting RadioButton1 (to indicate an addition operation), then clicking on Button1 (to calculate). The script captures the HTTP response from the Web server, displays the entire response, and searches the response to see if 8.0000 (the result of 5 + 3) is in TextBox3, the control created to display the calculation result.

The Application under Test

The target of my test automation is a basic Web application called MiniCalc, which I first introduced in my March 2008 column (see msdn2.microsoft.com/magazine/cc337896.aspx). The complete code for my MiniCalc Web application is shown in Figure 3.

Figure 3 MiniCalc Source Code

<%@ Page Language="C#" %>
<script language="C#" runat="server">
  private void Button1_Click(object sender, System.EventArgs e)
  {
    int alpha = int.Parse(TextBox1.Text.Trim());
    int beta = int.Parse(TextBox2.Text.Trim());
    
    if (RadioButton1.Checked) {
      TextBox3.Text = Sum(alpha, beta).ToString("F4");
    }
    else if (RadioButton2.Checked) {
      TextBox3.Text = Product(alpha, beta).ToString("F4");
    }
    else
      TextBox3.Text = "Select method";
  }
  private static double Sum(int a, int b) {
    double ans = a + b;
    return ans;
  }
  private static double Product(int a, int b) {
    double ans = a * b;
    return ans;
  }
</script>

<html>
  <head>
    <style type="text/css">
      fieldset { width: 16em }
      body { font-size: 10pt; font-family: Arial }
    </style>
    <title>Default.aspx</title>
  </head>
  <body bgColor="#ccffff">
    <h3>MiniCalc by ASP.NET</h3>
    <form method="post" name="theForm" id="theForm" runat="server"
       action="Default.aspx"> 
      <p><asp:Label id="Label1" runat="server">
         Enter integer:&nbsp&nbsp</asp:Label>
        <asp:TextBox id="TextBox1" width="100" runat="server" />
      </p>
      <p><asp:Label id="Label2" runat="server">
          Enter another:&nbsp</asp:Label>
         <asp:TextBox id="TextBox2" width="100"  runat="server" />
      </p>
      <fieldset>
        <legend>Arithmentic Operation</legend>
        <p><asp:RadioButton id="RadioButton1" GroupName="Operation"
            runat="server"/>Addition</p>
        <p><asp:RadioButton id="RadioButton2" GroupName="Operation" 
            runat="server"/>Multiplication</p>
        <p></p>
      </fieldset>
      <p><asp:Button id="Button1" runat="server" text=" Calculate "
        onclick="Button1_Click" /> </p>
      <p><asp:TextBox id="TextBox3" width="120"  runat="server" /></p>
    </form>
  </body>
</html>

The most important parts of this source code for you to note are the IDs of my ASP.NET server controls. I use default IDs: Label1 (user prompt), TextBox1 and TextBox2 (input for two integers), RadioButton1 and RadioButton2 (to choose addition or multiplication), Button1 (calculate), and TextBox3 (result). To perform automated HTTP request/response testing on an ASP.NET application, you need to know the IDs of the application's controls. And while you can view the IDs of my test application via the downloadable source code, even if you are testing a Web application you didn't write, you can examine the IDs by viewing the application's source using the Internet Explorer® View | Source command.

The action attribute of my <form> element is set to Default.aspx. Every time a user submits a request, the same Default.aspx code is executed. Because HTTP is a stateless protocol, ASP.NET gives the effect of being a stateful application by maintaining the application's state in a hidden value called ViewState.

A ViewState value is a Base64-encoded string that maintains the state of an ASP.NET page through multiple request/response round-trips. ASP.NET uses hidden input controls to maintain the ViewState. Similarly, an EventValidation value has been added in ASP.NET 2.0 that is used for security purposes to help prevent script-insertion attacks. These two mechanisms are key to programmatically posting data to an ASP.NET 2.0 Web application.

Fundamental ASP.NET Request/Response Testing

The Windows PowerShell script that produced the screenshot in Figure 2 illustrates all the fundamental techniques you need to perform basic HTTP request/response testing for ASP.NET Web applications. And this script can serve as a foundation for more sophisticated testing.

Notice that even though my script is in the current directory, for security reasons I must still specify the script's location (.\test.ps1 rather than simply test.ps1). Also note that, by default, Windows PowerShell does not allow script execution at all. To enable script execution, I entered the command Set-ExecutionPolicy unrestricted in a previous Windows PowerShell session. The PowerShell execution policy stays in effect for a particular user across all new shell instances and user sessions, until the policy is explicitly changed.

The overall structure for my test.ps1 script is:

# file: test.ps1
function main()
{
  # send request, get response, check response
}
function getVSandEV($url)
{
  # get initial ViewState and EventValidation values
}
main
# end script

The first line is a Windows PowerShell comment. Next I have a main function that does most of the work, followed by a helper function named getVSandEV. (I'll dig into this helper function in a moment.) The use of a "main" function is optional. And note that main is not a reserved word, so I could have used some other identifier if I had wanted.

In terms of script organization, Windows PowerShell is quite flexible—I could have placed all my code in test.ps1, without any program-defined functions, or I could have broken my main function down a bit further by writing additional helper functions.

Notice the second-to-last line of my script. Since main is not a predefined program entry point, I must explicitly invoke the main function.

Inside my main function, I begin with this statement:

$url = "https://localhost/MiniCalc/Default.aspx"

This assigns a string value to a variable named $url. (Technically speaking, $url is an object, but, even so, it is considered acceptable to call simple Windows PowerShell objects variables.) Windows PowerShell variables begin with the $ character, making them quite easy to identify in a script.

The Windows PowerShell scripting language is an explicitly typed language that supports type inference. If a Windows PowerShell variable is not explicitly typed, the Windows PowerShell execution engine will do its best to infer a type. In this case, variable $url will be interpreted as a string variable because of the double-quote characters delimiting its value. But I could have supplied an explicit type qualifier like this:

[string] $url = ` "https://localhost/MiniCalc/Default.aspx"

Since Windows PowerShell is built on the Microsoft® .NET Framework, I can use any .NET type (such as Int32 or String) or equivalent C# mapping alias (such as int or string). Adding an explicit type to a variable provides some runtime error checking and improved readability, but at the expense of some extra typing.

My second statement prints a message to the shell, shown here:

write-host ` "`nWeb Application under test is: " $url

Here I use the Write-Host cmdlet to display a message. Windows PowerShell uses both single quotes and double quotes for strings. I have used double quotes here so I can embed the `n sequence, which is the Windows PowerShell method of placing a "new line" character in a string. (Strings that are delimited by single quotes are considered literals.)

Inside the Helper Function

Next I call my program-defined function to determine the initial ViewState and EventValidation values for the MiniCalc Web app:

$vs,$ev = getVSandEV($url)

This indicates that I am assigning two return values from the getVSandEV function to two variables, $vs and $ev. (Actually, as I'll explain in a moment, the function's return value is a single array with two values.)

The entire getVSandEV helper function is shown in Figure 4. The getVSandEV function returns initial ViewState and EventValidation values for the specified ASP.NET Web page. I have not specified a type for my input parameter, $url, but I could have explicitly typed the parameter for a measure of runtime error checking.

Figure 4 Code for getVSandEV Helper Function

function getVSandEV($url)
{
  $wc = new-object net.WebClient
  $probe = $wc.downloadData($url)
  $s = [text.encoding]::ascii.getString($probe)

  $start = $s.indexOf('id="__VIEWSTATE"', 0) + 24
  $end = $s.indexOf('"', $start)
  $vs = $s.substring($start, $end-$start)

  write-host "`nInitial ViewState is: " $vs

  $start = $s.indexOf('id="__EVENTVALIDATION"', 0) + 30
  $end = $s.indexOf('"', $start)
  $ev = $s.substring($start, $end-$start)

  write-host "`nInitial EventValidation is: " $ev

  return ($vs,$ev)
}

The first line in the body of getVSandEV instantiates a WebClient object, shown here:

$wc = new-object net.WebClient

I use the new-object cmdlet to create a WebClient object (the WebClient class is part of the System.Net namespace). I could also have explicitly specified a type for my $wc object, like so:

[net.WebClient] $wc = new-object net.WebClient

The WebClient class provides an easy way to send an HTTP request and retrieve the resulting HTTP response. I can accomplish this with a single statement:

$probe = $wc.downloadData($url)

I call the DownloadData method of the WebClient class, and the return value is a byte array that holds the entire HTTP response from the application at the given URL. I store this response into a byte array named $probe, which I could have also explicitly typed as [byte[]] $probe. Next I convert the HTTP response from an array of bytes to a string:

$s = [text.encoding]::ascii.getString($probe)

The syntax here is a bit tricky at first glance: text.encoding is a class—the Encoding class of the System.Text namespace. The ascii part is a static property of the Encoding class and the getString part is a method. I could have converted my byte array to a string in two steps instead of one, like this:

$e = [text.encoding]::ascii
$s = $e.getString($probe)

Extracting ViewState and EventValidation

At this point, variable $s holds the entire HTTP response as a string. This string includes the ViewState value and looks something like the following:

<input type="hidden" name="__VIEWSTATE"
 id="__VIEWSTATE" value="/wEPDwUK ... Bbzmv" />

Now I need to extract the ViewState value from $s. I identify the index position within $s where the ViewState value begins:

$start = $s.indexOf('id="__VIEWSTATE"', 0) + 24

As shown above, I use the IndexOf method to search for the string id="__VIEWSTATE". The 0 argument means begin searching $s at index position 0—the beginning of the response string. Notice that since my target string contains double quote characters, I delimit the target string using single quote characters. Once I find where my target string begins, the actual ViewState value starts 24 characters from that index. (Note that "__VIEWSTATE" has two leading underscore characters, not just one.)

This is a pretty crude way to parse for the initial ViewState value, and it makes my code more brittle. However, I'm just performing quick and easy test automation, and because I can quickly and easily modify my script, I'm willing to accept the risk that my code may break if the storage format for the view state changes.

Now I find where within $s the ViewState value ends:

$end = $s.indexOf('"', $start)

I search for the first occurrence of any double quote character found after the beginning of the ViewState value and store that location into variable $end. Now that I know where the ViewState value begins and ends within the response string $s, I can extract the value using the SubString method:

$vs = $s.substring($start, $end-$start)
write-host "`nInitial ViewState is: " $vs

The two arguments to the SubString method are the index within $s from which to begin extracting and the number of characters to extract (not the index to end extracting, which is a common mistake). At this point, variable $vs holds the initial ViewState value for the target page. Extracting the EventValidation value follows the same pattern:

$start = $s.indexOf('id="__EVENTVALIDATION"', 0) + 30
$end = $s.indexOf('"', $start)
$ev = $s.substring($start, $end-$start)
write-host "`nInitial EventValidation is: " $ev

Here I reuse my $start and $end variables. The only difference between the two is that the EventValidation value in the response string $s begins 30 characters after the start of the id= "__EVENTVALIDATION" string.

Now I have two values, $vs and $ev, that I want to return from my getVSandEV function. To do this, I use a simple but powerful Windows PowerShell function:

return ($vs,$ev)

The parentheses specify a Windows PowerShell array. And since Windows PowerShell is based on a pipeline architecture, I can omit the return keyword if I want. But I like to use "return" for clarity since it is a familiar programming idiom.

Working with the ViewState and EventValidation Values

Now back to the main function in my test.ps1 script. The next three statements are:

[Reflection.Assembly]::LoadFile( `
  'C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Web.dll')`
  | out-null

$vs = [System.Web.HttpUtility]::UrlEncode($vs)
$ev = [System.Web.HttpUtility]::UrlEncode($ev)

Both my ViewState and EventValidation values are base64-encoded strings. Base64 encoding uses 64 characters, including uppercase A-Z, lowercase a-z, digits 0-9, the + character, and the / character. (The = character is used in Base64 for padding.) Because some of these characters are not permitted in an HTTP request stream, I therefore need to perform a UrlEncode operation on my ViewState and EventValidation values.

UrlEncoding converts potentially troublesome characters into a three-character sequence beginning with the % character. For example, a raw > character is encoded as %3D and a blank space is encoded as %20. When a Web server receives an HTTP request containing any of these special three-character sequences, the server decodes the sequences back to raw input.

The .NET Framework has a static UrlEncode method in the HttpUtility class, which is in the System.Web assembly. However, unlike many .NET namespaces, System.Web is not accessible to Windows PowerShell by default. So, before I can use UrlEncode, I must load the System.Web assembly. To do this, I use the static LoadFile method in the System.Reflection namespace, and pipe that command to the out-host cmdlet to suppress noisy progress messages as System.Web is loaded and registered.

Now I can prepare my post data. HTTP post data is a set of name-value pairs connected by the ampersand character, like so:

$postData = `
  'TextBox1=5&TextBox2=3&Operation=RadioButton1&Button1=clicked'
write-host "`nPosting: " $postData

The first two name-value pairs of the post string are fairly self-explanatory and simply set TextBox1 and TextBox2 to 5 and 3, respectively. The third name-value pair, Operation=RadioButton1, is how I simulate a user selecting a RadioButton control—in this case, the RadioButton that corresponds to addition. You might have guessed incorrectly (as I originally did) that the way to set the radio button would be to use a syntax like RadioButton1=checked. But RadioButton1 is a value of the Operation control, not a control itself.

The fourth name-value pair, Button1=clicked, is somewhat misleading. I need to supply a value for Button1 to simulate that it is being clicked, but any value will work. I could have used Button1=foo (or even just Button1=), but Button1=clicked provides more clarity.

Next I add my Url-encoded ViewState and EventValidation values to the post data and convert to a byte array:

$postData += '&__VIEWSTATE=' + $vs
$postData += '&__EVENTVALIDATION=' + $ev
$buffer = [text.encoding]::ascii.getbytes($postData)

Windows PowerShell supports the familiar += operator for string concatenation. Note that both of these strings have two leading underscore characters. The GetBytes method accepts a string and returns that string as an array of bytes.

Now I create an HttpWebRequest object, as shown here:

[net.httpWebRequest] $req = [net.webRequest]::create($url)
$req.method = "POST"
$req.ContentType = "application/x-www-form-urlencoded"
$req.ContentLength = $buffer.length
$req.TimeOut = 5000

To create an HttpWebRequest object, use an explicit Create method. Note that the Create method belongs to the System.Net.WebRequest class. Since this is potentially confusing, I explicitly supply a type ([Net.HttpWebRequest]) for my $req object.

The next four lines of code prepare the HttpWebRequest object by specifying the type (POST), content type (application/ x-www-form-urlencoded), content length (calculated on the fly), and a server time-out value (5000, which is 5,000 milliseconds or 5 seconds). Now I send the HTTP request to my MiniCalc Web application, like this:

$reqst = $req.getRequestStream()
$reqst.write($buffer, 0, $buffer.length)
$reqst.flush()
$reqst.close()

The HttpWebRequest class does not have a Send method. So I create a request stream object from my HttpWebRequest object and then write to the stream. The Write method prepares the request; the Flush method, then, actually causes the HTTP request to be sent. Because the Close method performs a Flush automatically, I could have also omitted the $reqst.flush() statement.

Now I need to capture the HTTP response, like this:

[net.httpWebResponse] $res = $req.getResponse()
$resst = $res.getResponseStream()
$sr = new-object IO.StreamReader($resst)
$result = $sr.ReadToEnd()
write-host "`nHTTP Response is " $result 

In these lines, I first create an HttpWebResponse object ($res) from my HttpWebRequest object ($req). Here I explicitly supply a type for clarity. Then I create a System.IO.Stream object ($resst). I then create a StreamReader object to process the response stream and use the ReadToEnd method to capture the entire HTTP response as a string into variable $result. This programming paradigm may appear a bit awkward at first, but the pattern quickly becomes familiar after you've used it a few times.

Now that I have my HTTP response, I can examine it for an expected value:

write-host "`nLooking for 8.000"
if ($result.indexOf('value="8.0000"') -ge 0) {
  write-host "`nFound 8.0000 -- Test scenario Pass`n" `
    -foregroundcolor green
}
else {
  write-host "`nDid not find 8.0000 -- Test scenario FAIL`n" `
    -foregroundcolor red
}

I use the String.IndexOf method to look for value=8.0000 in TextBox3 (since I posted data that equals 5 + 3). Note that Windows PowerShell uses Boolean comparison operators such as -ge (rather than >=), -eq (rather than ==), and so on. The last two lines of my test automation script clean up the resources:

$sr.close()
$resst.close()

I simply close the StreamReader object and the associated request Stream. Of course, I could have done this earlier (before determining my pass/fail result).

Extending the Request/Response Automation

The techniques I've discussed provide all the basics you need to perform lightweight HTTP request/response testing of an ASP.NET Web application. However, there are many ways you can extend this code to create a simple test harness (see Figure 5).

Figure 5 A Simple Test Harness

Figure 5** A Simple Test Harness **

You may want to parameterize your Windows PowerShell scripts. In my example, the URL of the target app is hardcoded into the script. However, you can write your script to accept a URL from the command line:

   #file: harness.ps1
   param($url = $(throw "URL required"))
   (etc.)

You could then call your script, like this:

   PS C:\MyScripts> .\harness.ps1 `
    'https://localhost/MiniCalc/Default.aspx'

If you use the param statement, it must be the first non-comment statement in a Windows PowerShell script. The throw statement makes a command-line argument required—if an argument is not given, the script will throw an exception.

You might also want to parameterize your test case data. One simple approach would be to place the test case data inside your script in an array like the following:

$cases = (
  ('0001,TextBox1=5&TextBox2=3&Operation=' +
    'RadioButton1&Button1=clicked,value="8.0000"'),
  ('0002,TextBox1=5&TextBox2=3&Operation=' +
    'RadioButton2&Button1=clicked,value="15.0000"'),
  ('0003,TextBox1=0&TextBox2=0&Operation=' +
    'RadioButton1&Button1=clicked,value="0.0000"')
)

The @ character forces variable $cases to be an array type. But it really is optional here since there is no potential ambiguity in this scenario. Then you can iterate through each test case, like this:

foreach ($case in $cases) {
  $caseID,$input,$expected = $case.split(",")
  # get ViewState and EventValidation values
  # construct post data
  # send HTTP request, get response
  # determine pass/fail
}

The foreach loop will extract each string in turn from array $cases and store the current string into $case. Then the split method breaks the current string at each comma character and stores the results into variables $caseID, $input, and $expected.

Windows PowerShell has several error-handling features, including a trap statement to deal with exceptions. For example, in test automation, you typically want your harness to continue executing even if one test case throws an exception. To do this in Windows PowerShell, you can use the trap and continue statements:

function main(){
  # test cases here
  foreach ($case in $cases) {
    # run test case
    trap {
      write-host "Fatal exception caught!"
      continue
    }
  }
}

Of course, there are many other modifications you can employ, such as keeping track of the number of test cases that pass and fail, writing test results to an external file or database, and so on.

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

Dr. 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 is the author of .NET Test Automation Recipes, and he can be reached at jmccaffrey@volt.com or v-jammc@microsoft.com.