January 2010

Volume 25 Number 01

Test Run - Web Application Request-Response Testing with JavaScript

By Dr. James McCaffrey | January 2010

Download the Code Sample.

In this month’s column I explain how to write simple and effective browser-based request-response test automation using JavaScript. The best way for you to see where I’m headed is to take a look at the screenshots in Figures 1 and 2. Figure 1 shows a simple but representative ASP.NET Web application under test named Product Search. A user enters some search string into the application’s single textbox control, and specifies whether the search is to be performed in a case-sensitive manner using two radio button controls. Search results are displayed in a listbox control.

Figure 1 Product Search Web Application Under Test

image: Product Search Web Application Under Test

Although the example Web application under test is based on ASP.NET, the technique I present in this article can be used to create test automation for Web applications written using most dynamic page generation technologies, including PHP, JSP, CGI and others.

Figure 2 shows the request-response test automation in action. Notice that the test automation harness is browser-based. One of the advantages of the technique I present here compared to alternative approaches is that the technique can be used with most major Web browsers, and can be executed on test host machines running most operating systems.

Figure 2 Request-Response Test Run

image: Request-Response Test Run

The test automation harness consists of a single HTML page that houses a relatively short set of JavaScript functions. Notice that the first line of test run output indicates that the test automation is using the jQuery library. The harness reads test case input data, which corresponds to user input, and programmatically posts that input data to the Product Search Web application. The harness accepts the resulting HTTP response data and examines that response for an expected value in order to determine a test case pass/fail result.

In the sections of this article that follow, I first briefly describe the Web application under test shown in Figure 1 so that you’ll understand which factors are relevant to HTTP request-response testing. Next I explain in detail the test harness code shown running in Figure 2 so that you’ll be able to modify the harness to meet your own needs. I conclude with a few comments about when browser-based request-response test automation with JavaScript is appropriate and when alternative approaches might be more suitable.

This article assumes you have intermediate level JavaScript and ASP.NET skills, but even if you’re a beginner with these technologies you should be able to follow my explanations without too much difficulty.

Building the Web Application

I used Visual Studio 2008 to create the Product Search Web application under test. In order to leverage Visual Studio’s ability to configure a Web site, I selected File | New | Web Site from the main menu bar. Next I selected the Empty Web Site option from the resulting new Web site dialog box. I specified an HTTP location on my local machine to create a complete ASP.NET Web site, rather than specifying a file system location to use the built-in Visual Studio development server. I selected the C# language for my logic code.

After clicking OK, Visual Studio created the empty ProductSearch Web site. In the Solution Explorer window, I right-clicked on the ProductSearch project and selected Add New Item from the context menu. I selected the Web Form item and accepted the default page name of Default.aspx and clicked Add to generate the page. Next I created the simple UI for the Web application under test, as presented in Figure 3.

Figure 3 Web App UI

<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">
  <title>Product Search</title>
</head>
<body bgcolor="#ccbbcc">
  <form id="form1" runat="server">
  <div>
    <asp:Label ID="Label1" runat="server" Text="Find:" 
      Font-Names="Arial" Font-Size="Small">
    </asp:Label>&nbsp;&nbsp;
    <asp:TextBox ID="TextBox1" runat="server" Width="114px">
    </asp:TextBox>&nbsp;&nbsp;
    <asp:Button ID="Button1" runat="server" onclick="Button1_Click" 
      Text="Go" />
    <br />

    <asp:RadioButtonList ID="RadioButtonList1" runat="server" 
      Font-Names="Arial" Font-Size="Small">
    <asp:ListItem>Case Sensitive</asp:ListItem>
    <asp:ListItem Selected="True">Not Case Sensitive</asp:ListItem>
    </asp:RadioButtonList>
  </div>
  <asp:ListBox ID="ListBox1" runat="server" Height="131px" Width="246px"
    Font-Names="Courier New" Font-Size="Small">
  </asp:ListBox>
  </form>
</body>
</html>

As I will explain shortly, when creating HTTP request-response test automation you must know the IDs of any of the input controls that you wish to simulate user input on. In this case I have access to the source code of the application under test, but even if you do not have source code access you can always determine input control IDs using a Web browser’s view-source functionality. Notice that the two radio button controls are actually represented by a single input control with ID RadioButtonList1, rather than by two controls as you might have guessed.

I added the application logic directly into the Defaut.aspx file rather than using the code-behind mechanism. At the top of the page I created a script block to hold the application’s logic code:

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
  // ...
</script>

I added a small class into the script block to represent a Product object:

public class Product {
  public int id;
  public string desc;
  public Product(int id, string desc) {
    this.id = id; this.desc = desc;
  }
}

Then I added an internal application-scope ArrayList object to simulate an external data store:

public static ArrayList data = null;

In most realistic Web application scenarios, data stores are usually external, such as an XML file or SQL Server database. However, when performing HTTP request-response testing, the location of an application’s data store is irrelevant to some extent. The HTTP request has no knowledge of the data store’s location, and the HTTP response typically contains only HTML. Next I added some code to populate the internal data store with Product items: 

protected void Page_Load(object sender, EventArgs e) {
  if (!IsPostBack) {
    data = new ArrayList();
    Product p1 = new Product(111, "Widget");
    Product p2 = new Product(222, "Gizzmo");
    Product p3 = new Product(333, "Thingy");
    data.Add(p1); data.Add(p2); data.Add(p3);
  }
}

Finally, I placed all the application logic into the event handler for the Button1 click event. I begin by clearing the ListBox1 result area and fetching user input:

ListBox1.Items.Clear();
string filter = TextBox1.Text.Trim();
string sensitivity = RadioButtonList1.SelectedValue;

The sensitivity string variable will hold either “Case Sensitive” or “Not Case Sensitive.”

Next I place header information into the ListBox1 result area and declare a string to hold a Product search result and initialize a counter to track how many Product items match the search filter:

ListBox1.Items.Add("ID   Description");
ListBox1.Items.Add("================");
string resultRow;
int count = 0;

I iterate through each Product object in the ArrayList data store checking to see if the search filter string matches the current object’s description field:

foreach (Product p in data) {
  resultRow = "";
  if (sensitivity == "Not Case Sensitive" &&
    p.desc.IndexOf(filter, 
    StringComparison.CurrentCultureIgnoreCase) >= 0) {
    resultRow = p.id + " " + p.desc; ++count;
  }
  else if (sensitivity == "Case Sensitive" && 
    p.desc.IndexOf(filter) >= 0) {
    resultRow = p.id + " " + p.desc; ++count;
  }
  if (resultRow != "") ListBox1.Items.Add(resultRow);
}

For each product that matches the search filter, I build a result string and increment the hit counter. Notice that the IndexOf method is conveniently overloaded to accept a case-sensitivity argument.

The application logic finishes by adding a blank line and a count summary to the ListBox1 display area:

ListBox1.Items.Add("");
ListBox1.Items.Add("Found " + count + " matching items");

In order to keep the size of the Web application as small and simple as possible, I have taken many shortcuts you wouldn’t use in a production environment. In particular I have not provided any error checking or handling.

Request-Response Test Automation

I created the test harness page shown running in Figure 2 using Notepad. The overall structure of the harness is shown in Figure 4.

Figure 4 Test Harness Structure

<html>
<!-- RequestResponseTests.html -->
<head>
  <script src=’https://localhost/TestWithJQuery/jquery-1.3.2.js’>
  </script>
  <script type="text/javascript">
    $(document).ready(function() {
      logRemark("jQuery Library found and harness DOM is ready\n");
    } );
  
    var targetURL = ‘https://localhost/TestWithJQuery/ProductSearch/Default.aspx’;

    var testCaseData =
[ ‘001,TextBox1=T&RadioButtonList1=Case+Sensitive&Button1=clicked,333 Thingy’,
‘002,TextBox1=t&RadioButtonList1=Not+Case+Sensitive&Button1=clicked,Found 2 matching items’ ];
        
    function runTests() {
      try {
        logRemark(‘Begin Request-Response with JavaScript test run’);
        logRemark("Testing Product Search ASP.NET application\n");
        // ...
        logRemark("\nEnd test run");
      }
      catch(ex) {
        logRemark("Fatal error: " + ex);
      }
    }
    
    function getVS(target) {
      // ...  
    }

    function getEV(target) {
      // ...  
    }

    function sendAndReceive(target, rawVS, rawEV, inputData) {
      // ...  
    }

    function logRemark(comment) {
      // ...  
    }

  </script>
</head>
<body bgcolor="#66ddcc">
  <h3>Request-Response Test Harness Page</h3>
  <p><b>Actions:</b></p><p>
  <textarea id="comments" rows="24" cols=63">
  </textarea></p>
  <input type="button" value="Run Tests" onclick="runTests();" /> 
</body>
</html>

The harness UI code in the body element at the bottom of the page consists only of some text, a textarea element to display information and a button to start the test automation.

The test harness structure begins by using the script element src attribute to reference the jQuery library. The jQuery library is an open source collection of JavaScript functions available from jquery.com. Although jQuery was created with Web development in mind, the library contains functions that make it well suited for lightweight request-response test automation. Here I point to a local copy of version 1.3.2 of the library. For test-automation purposes, using a local copy of the library is more reliable than pointing to a remote copy. Next I use the $(document).ready jQuery idiom to make sure that my harness can access the library and that the harness DOM is loaded into memory.

After setting up a variable targetURL that points to the Web application under test, I hard code internal comma-delimited test cases into a string array named testCaseData. Here I have just two test cases, but in a production environment you might have hundreds of cases. External test case data is often preferable to internal test case data because external data can be more easily modified and shared. However, because the technique I’m presenting here is lightweight, internal test case data is a reasonable design choice.

The first field in a test case is a case ID number. The second field is raw request data to send to the application under test. The third field is an expected result.

How did I know the format of the request data? The easiest way to determine the format of HTTP request data is to perform preliminary experimentation with the application under test by examining actual request data using an HTTP logger tool such as Fiddler.

Running the Tests

The main harness control function is named runTests. The runTests function uses a top-level try-catch mechanism to provide rudimentary error handling. I use an auxiliary function named logRemark to display information to the harness textarea element. The harness uses helper functions getVS and getEV to get the current ViewState and EventValidation values of the ASP.NET Web application under test. These application-generated Base64-encoded values act primarily as state and security mechanisms, and must be sent as part of any HTTP POST request. The sendAndReceive function performs the actual HTTP request and returns the corresponding HTTP response. The runTests function iterates through each test case:

for (i = 0; i < testCaseData.length; ++i) {
  logRemark("==========================");
  var tokens = testCaseData[i].split(‘,’);
  var caseID = tokens[0];
  var inputData = tokens[1];
  var expected = tokens[2];
  ...

I use the built-in split function to separate each test case string into smaller pieces. Next I call the getVS and getEV helper functions:

logRemark(‘Case ID     : ‘ + caseID); 
logRemark(‘Fetching ViewState and EventValidation’);
var rawVS = getVS(targetURL);
var rawEV = getEV(targetURL);

The main processing loop continues by calling the sendAndReceive function and examining the resulting HTTP response for the associated test case expected value:

var response = sendAndReceive(targetURL, rawVS, rawEV, inputData);
logRemark("Expected    : ‘" + expected + "’");
if (response.indexOf(expected) >= 0)
  logRemark("Test result : **Pass**");
else if (response.indexOf(expected) == -1)
  logRemark("Test result : **FAIL**");
} // main loop

The getVS helper function relies on the jQuery library:

function getVS(target) {
  $.ajax({
    async: false, type: "GET", url: target,
    success: function(resp) {
      if (resp.hasOwnProperty("d")) s = resp.d;
      else s = resp;
         
      start = s.indexOf(‘id="__VIEWSTATE"’, 0) + 24;
      end = s.indexOf(‘"’, start);
    }
  });
  return s.substring(start, end);
}

The main idea of the getVS function is to send a priming GET request to the application under test, fetch the response and parse out the ViewState value. The $.ajax function accepts an anonymous function. The async, type and URL parameters should be fairly self-explanatory. The hasOwnProperty(“d”) method of the response resp object is essentially a security mechanism present in the Microsoft .NET Framework 3.5 and is not necessary in this situation.

I extract the ViewState value by looking for the start of the attribute, then counting over 24 characters to where the ViewState value actually begins. The getEV function code is exactly the same as the getVS code except that the EventValidation value starts 30 characters from the initial id=EVENTVALIDATION attribute. Having separate getVS and getEV functions gives you flexibility but requires two separate priming requests. An alternative is to refactor getVS and getEV into a single helper function.

The sendAndReceive helper function executes the actual HTTP request and fetches the resulting response. The function begins by converting the raw ViewState and EventValidation strings into URL-encoded strings, and then constructs the data to post to the Web application:

function sendAndReceive(target, rawVS, rawEV, inputData) {
  vs = encodeURIComponent(rawVS);
  ev = encodeURIComponent(rawEV);
  postData = inputData + ‘&__VIEWSTATE=’ + vs +
    ‘&__EVENTVALIDATION=’ + ev;
  ...

The built-in encodeURIComponent function encodes characters that are not legal values in post data into an escape sequence. For example, the ‘/’ character is encoded as %2F. After a logging message, sendAndReceive uses the $.ajax method to create an HTTP POST request:

logRemark("Posting " + inputData);
$.ajax({
  async: false,
  type: "POST",
  url: target,
  contentType: "application/x-www-form-urlencoded",
  data: postData,
  ...

The $.ajax method was created primarily to send asynchronous XML HTTP requests, but by setting the async parameter to false the method can be used to send standard synchronous requests. Neat! You can think of the content-type parameter value as a magic string that simply means data posted from an HTML form element. The sendAndReceive function uses the same pattern as getVS to grab the associated HTTP response:

success: function(resp, status) {
      if (resp.hasOwnProperty("d")) s = resp.d;
      else s = resp;
    },
    error: function(xhr, status, errObj) {
      alert(xhr.responseText);
    }
  });
  return s;
}

I also use the optional error parameter to display any fatal errors in an alert box.

The final function in the test harness is the logRemark utility:

function logRemark(comment) {
  var currComment = $("#comments").val();
  var newComment = currComment + "\n" + comment;
  $("#comments").val(newComment);
}

I use jQuery selector and chaining syntax to get the current text in the textarea element, which has an ID of comments. The ‘#’ syntax is used to select an HTML element by ID, and the val function can act as both a value setter and getter. I append the comment parameter value and a newline character to the existing comment text, and then use jQuery syntax to update the textarea element.

Alternatives

The main alternative to the browser-based, JavaScript language approach I’ve presented in this article is to create a shell-based harness using a language such as C#. Compared to a shell-based approach, the browser-based approach is most useful when you’re working in a highly dynamic environment where your test automation has a short lifespan. Additionally, the browser-based approach presented here is quite platform-independent. The technique will work with any browser and OS combination that supports the jQuery library and JavaScript.


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