December 2010

Volume 25 Number 12

Test Run - Web Application UI Testing with jQuery

By James McCaffrey | December 2010

The jQuery library is an open source collection of JavaScript functions. Although jQuery was created with Web development in mind, the library has several characteristics that make it well-suited for lightweight Web application UI test automation. In this month’s column I’ll show you how to do just that.

The best way for you to see where I’m headed is to examine the screenshot in Figure 1, which shows UI test automation with jQuery in action. The test harness is hosted by Internet Explorer and consists of an HTML page named UITestHarness.html.

The harness page is really just a container with two HTML frame elements. The frame on the right holds the Web application under test, in this case a simple but representative ASP.NET calculator application named MiniCalc. The frame on the left holds an HTML page named TestScenario001.html, which consists of a TextArea element to display progress messages, a Button element to manually launch the automation, and jQuery-based JavaScript functions that manipulate the Web application under test and check the resulting state of the application to determine a pass/fail result.

image: UI Test Automation with jQuery

Figure 1 UI Test Automation with jQuery

The jQuery library is also well-suited for HTTP request-response testing, and I addressed request-response testing with jQuery in the January 2010 Test Run column (msdn.microsoft.com/magazine/ee335793).

This article assumes you have basic familiarity with ASP.NET technology and intermediate JavaScript programming skills, but does not assume you have any experience with the jQuery library. However, even if you’re new to ASP.NET and test automation in general, you should still be able to follow this month’s column without too much difficulty.

In the sections that follow, I’ll first describe the MiniCalc application so you’ll know exactly how the implementation of the application under test is related to the UI test automation. Next, I’ll walk you through the details of creating lightweight jQuery-based UI test automation as shown in Figure 1. I’ll wrap up by describing how you can extend the techniques I’ve presented to meet your own needs, and I’ll discuss the advantages and disadvantages of jQuery UI test automation compared to alternative approaches. I think the techniques presented here are interesting and can be a useful addition to your testing, development and management toolsets.

The Application Under Test

Let’s take a look at the code for the MiniCalc ASP.NET Web application, which is the target of my jQuery-based UI test automation.

I created the MiniCalc application using Visual Studio 2008. After launching Visual Studio I clicked File | New | Web Site. To avoid the ASP.NET code-behind mechanism and keep all the code for my Web application in a single file, I selected the Empty Web Site option. Next, I selected the HTTP mode option (rather than File mode) from the Location field drop-down and specified the location as:

https://localhost/TestWithJQuery/MiniCalc

I decided to use C# for the MiniCalc app logic. The test automation techniques presented here will work with ASP.NET Web applications written with C# and Visual Basic as well as Web applications created using technologies such as classic ASP, CGI, PHP, JSP, Ruby and so on.

I clicked OK on the New Web Site dialog to configure IIS and generate the structure of my Web app. Next, I went to the Solution Explorer window, right-clicked the MiniCalc project name, and selected Add New Item from the context menu. I then selected Web Form from the list of installed templates and accepted the Default.aspx file name. I cleared the “Place code in separate file” option and then clicked the Add button.

Next, I double-clicked on the Default.aspx file name in Solution Explorer to load the template-generated code into the text editor. I deleted all the template code and replaced it with the code shown in Figure 2.

Figure 2 MiniCalc Web Application Under Test Source

<%@ Page Language="C#" %>
<script runat="server">
  static Random rand = null;
  private void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack) 
      rand = new Random(0);
  }
  private void Button1_Click(object sender, System.EventArgs e)
  {
    int randDelay = rand.Next(1, 6); // [1-5]
    System.Threading.Thread.Sleep(randDelay * 1000);
    int x = int.Parse(TextBox1.Text);
    int y = int.Parse(TextBox2.Text);
    if (RadioButton1.Checked)
      TextBox3.Text = (x + y).ToString("F4");
    else if (RadioButton2.Checked)
      TextBox3.Text = (x * y).ToString("F4");
  }
</script>
<html>
  (client-side JavaScript and UI elements here)
</html>

To keep the size of my source code small and easy to understand, I omitted normal error checking. The complete source code for the MiniCalc application and the test harness is available from code.msdn.microsoft.com/mag201012TestRun.

To write test automation for Web applications, in most cases you need to know the IDs of the various user controls. As you can see in Figure 2, I used TextBox1 and TextBox2 to hold two user integer input values—RadioButton1 and RadioButton2 to select addition or multiplication—and TextBox3 to hold the arithmetic calculation result.

When a user clicks on the Button1 control, the MiniCalc app first goes into a random delay of one to five seconds to simulate server-side processing of some sort, and then computes and displays either a sum or product of the two user input values.

Next, I decided to make the MiniCalc app asynchronous by using AJAX technology. To do that I needed a web.config file for the application, so, rather than create a web.config file manually from scratch, I hit the F5 key to instruct Visual Studio to build and run the app through the debugger. When Visual Studio prompted me for permission to add a web.config file, I clicked OK. Next, I added a ScriptManager server-side control to the MiniCalc application to enable AJAX:

<asp:ScriptManager ID="sm1" runat="server" EnablePartialRendering="true" />

Then I added the tags necessary to asynchronously update the TextBox3 result element in conjunction with the Button1 click event:

<asp:UpdatePanel ID="up1" runat="server">
<ContentTemplate>
<p><asp:TextBox id="TextBox3" width="120"  runat="server" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" />
</Triggers>
</asp:UpdatePanel>

If you examine Figure 1 closely, you can see that, to emphasize the fact that MiniCalc is an AJAX app, I placed a client-side page life counter in the UI. When an asynchronous request to MiniCalc returns, only TextBox3 is updated and the page life counter is not reset. The pageLife text box is defined as:

<input type="text" id="pageLife" size="1"/>

The associated client-side JavaScript is:

<script language="javascript">
  var count = 0;
  function updatePageLife() {
    ++count;
    var tb = document.getElementById("pageLife");
    tb.value = parseInt(count);
    window.setTimeout(updatePageLife, 1000);
  }
</script>

The counter is started up by the application onload event:

<body bgColor="#ccffff" onload="updatePageLife();">

Web Application UI Testing with jQuery

Now that you’ve seen the Web application under test, let’s dive right into the UI test automation code. The main test harness is simply an ordinary HTML page with two frame elements:

<html>
<!-- UITestHarness.html -->
<head>
  <title>Test Harness for MiniCalc AJAX Web App</title>
</head>
  <frameset cols="45%,*" onload="leftFrame.appLoaded=true">
    <frame src="https://localhost/TestWithJQuery/TestScenario001.html"
       name="leftFrame" >
    <frame src="https://localhost/TestWithJQuery/MiniCalc/Default.aspx"
       name="rightFrame">
  </frameset>
</html>

The frame named rightFrame hosts the Web application under test as is, without any modifications or test instrumentation. The frame named leftFrame hosts an HTML page named TestScenario001.html, which contains all the jQuery test automation code. Notice that when the frameset element onload event fires, a variable in the leftFrame page, named appLoaded, is set to true. This variable will be used to make sure that the test automation does not begin before the Web application under test is completely loaded into the test harness. The structure of the test scenario code is listed in Figure 3.

Figure 3 Structure of UI Test Automation Page

<html>
<!-- TestScenario001.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 testScenarioID = "Test Scenario 001";
    var maxTries = 20;
    var numTries;
    var polling = 500; // milliseconds
    var appLoaded = false;
    var started = false;
    
    function launch() {
      if (!started)
        runTest();
    }
    
    function waitUntilAppLoaded() {
      // Code
    }
    
    function runTest() {
      // Start automation
    }
    
    function step1() {
      // Manipulate state
    }
    function clickCalculate() {
      // Click the Calculate button
    }
    function checkControl(controlID, controlVal) {
      // Determine if control has specified value
    }
    
    function step2() {
      // Manipulate state
    }
    
    function callAndWait(action, checkControlFunc, controlID, controlVal,
      callbackFunc, pollTime) {
      // The heart of the automation
    }
    function doWait(checkControlFunc, controlID, controlVal, 
      callbackFunc, pollTime) {
      // Wait until Web app responds
    }
    
    function finish() {
      // Determine pass/fail result
    }
       
    function logRemark(comment) {
      // Utility logging function
    }
  </script>
</head>
<body bgcolor="#F5DEB3">
  <h3>This is the UI test scenario with jQuery script page</h3>
  <p>Actions:</p><p><textarea id="comments" rows="22" cols="34">
  </textarea></p>
  <input type="button" value="Run Test" onclick="runTest();" /> 
</body>
</html>

The test script begins by referencing the jQuery library:

<script src='https://localhost/TestWithJQuery/jquery-1.3.2.js'>

Here I point to a local copy of the jQuery library that I had downloaded from the jQuery Project Web site (jquery.com) and copied to the MiniCalc application root directory. I used jQuery version 1.3.2. The library is under constant development, so there will likely be a newer version available by the time you read this article. For more information about referencing the jQuery library in your code, see “Getting the jQuery Library.”


Getting the jQuery Library

You have a few options for the location of the jQuery library used by your application. As mentioned, you can download the latest version from jquery.com and use it from your local file system. The jQuery site has both development (uncompressed) and production (minified—white space removed—for a smaller 
footprint) downloads available. Just select the package you want and save the .js file to your project directory.

If your application host has an active Internet connection, an even easier option is to point to the most current version of jQuery from an online content delivery network (CDN). There are a number of sources you can use (including your own hosted version), but two highly available CDNs are the Microsoft AJAX Content Delivery Network (asp.net/ajaxlibrary/cdn.ashx) and the Google Libraries API (developers.google.com/speed/libraries/).

For example, you could use the minified version of jQuery from the Microsoft Ajax CDN with the following script tag:

<script 
  src="https://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js" 
  type="text/javascript">
</script>

Scott Guthrie has a useful blog post on using the Microsoft Ajax CDN for both jQuery and ASP.NET AJAX at tinyurl.com/q7rf4w.

In general, when using jQuery for test automation, using a local, unpacked copy of the library in the test harness is more reliable than using a remote or packed copy. For production applications, however, you’ll want to use a reliable hosted library.


Next, I use a standard jQuery idiom to determine if my automation has access to the jQuery library:

$(document).ready(function() {
  logRemark("jQuery Library found and harness DOM is ready\n");
} );

The jQuery ready function fires as soon as the containing document DOM is fully loaded into the test host memory and all DOM elements are available. If the jQuery library is not accessible—which typically happens when you specify an incorrect path to the library—an “Object expected” error will be thrown.

The ready function accepts an anonymous function as its single parameter. Anonymous functions are used frequently in test automation for both jQuery and JavaScript. You can think of an anonymous function as a function that’s defined on the fly using the function keyword.

Here’s an example for a function called logRemark:

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

In this situation I define a function that simply invokes a program-defined logging function called logRemark to display a message to the test harness that jQuery is available. I could also have used the intrinsic JavaScript alert function.

I begin by using the jQuery selector and chaining syntax to get the current text in the textarea with ID “comments.” The $ notation is a shortcut alias for the jQuery meta-class. The # syntax is used to select an HTML element by ID, and the val function can act as both a value setter and getter (a property in object-oriented programming terminology). I append the comment parameter and a newline character to the existing comment text, and then use jQuery syntax to update the TextArea element.

Next, I set up a few test automation global variables:

var testScenarioID = "Test Scenario 001";
var maxTries = 20;
var numTries;
var polling = 500;
var appLoaded = false;
var started = false;

Because my automation deals with an asynchronous application, I don’t use arbitrary time delays. Instead, I use a sequence of short (defined by variable polling) delays, checking repeatedly (variable numTries) to see if the value of some HTML element satisfies a Boolean condition, up to a maximum number of attempts (variable maxTries). In this test scenario I delay a maximum of 20 attempts at 500 ms delay per attempt for a total of 10 seconds. The appLoaded variable is used to determine when the Web app under test is fully loaded into the test harness. The started variable is used to coordinate test harness execution.

To manually start the automation you can click on the Run Test button:

<input type="button" value="Run Test" onclick="runTest();" />

The launch function shown in Figure 3 is used for full test automation, as I’ll explain shortly. The runTest function acts as the main coordinating function for the test automation:

function runTest() {
  waitUntilAppLoaded();
  started = true;
  try {
    logRemark(testScenarioID);
    logRemark("Testing 3 + 5 = 8.0000\n");
    step1();
  }
  catch(ex) {
    logRemark("Fatal error: " + ex);
  }
}

The runTest function begins by calling the waitUntilAppLoaded function, which is defined as:

function waitUntilAppLoaded() {
  if (appLoaded == true) return true;
  else window.setTimeout(waitUntilAppLoaded, 100);
}

Recall that the test scenario initializes variable appLoaded to false and that the harness frameset onload event sets appLoaded to true. Here I use the intrinsic setTimeout function to repeatedly pause for 100 ms until the value of appLoaded becomes true. Note that this approach could delay forever. To prevent this possibility, you may want to add a global counter and return false after some maximum number of delays.

After setting the global start variable, runTest displays some comments and invokes a step1 function in an exception handler wrapper. The harness structure I present here is only one possibility, and you can modify the harness organization to suit your programming style and test environment. With my structure I consider a test scenario as a sequence of state changes, each of which is represented by a stepX function.

The step1 function manipulates the state of the Web application under test by simulating user input, as shown in Figure 4.

Figure 4 Simulating Input with the step1 Function

function step1() {
  logRemark(
    "Entering 3 and 5 and selecting Addition");
  var tb1 = 
    $(parent.rightFrame.document).find('#TextBox1');
  tb1.val('3');
  var tb2 = 
    $(parent.rightFrame.document).find('#TextBox2');
  tb2.val('5');
  var rb1 = 
    $(parent.rightFrame.document).find('#RadioButton1');
  rb1.attr("checked", true);
  logRemark(
    "\nClicking Calculate, waiting for async response '8.0000'");
  callAndWait(clickCalculate, checkControl, "TextBox3", "8.0000", 
    step2, polling);
}

The jQuery syntax for accessing and manipulating HTML elements is consistent, elegant and, for the most part, browser-independent. Notice that to access the Web app loaded in the rightFrame element from code in the leftFrame element, I must use the parent keyword. Also notice that I must use the jQuery find filter.

When manipulating the TextBox1 and TextBox2 elements, I make the assumption that the Web app under test is fully loaded into the rightFrame element. This assumption may not be reasonable for applications with long load times, and in such situations you can place the jQuery selector code in a window.setTimeout delay loop, testing the target object against the built-in “undefined” value.

Because the MiniCalc application under test is an AJAX application, my harness cannot simply invoke the Calculate button click event, because the test harness code would continue execution without waiting for the application’s asynchronous response. So, I use a program-defined callAndWait function:

function callAndWait(action, checkControlFunc, controlID,
  controlVal, callbackFunc, pollTime) {
    numTries = 0;
    action();
    window.setTimeout(function(){doWait(
      checkControlFunc, controlID, controlVal, 
      callbackFunc, pollTime);}, pollTime);
}

The callAndWait function will invoke a function (the action parameter), go into a delay loop and pause a short amount of time (variable pollTime), and check to see if some application state is true by calling parameter function checkControlFunc with arguments of parameter controlID and controlVal. When checkControlFunc returns true, or a maximum number of delays have been executed, control will be transferred to parameter function callbackFunc.

The callAndWait function works hand-in-hand with a program-defined doWait function:

function doWait(checkControlFunc, controlID, 
  controlVal, callbackFunc, pollTime) {
  ++numTries;
  if (numTries > maxTries) finish();
  else  if (checkControlFunc(controlID, controlVal)) 
    callbackFunc();
  else window.setTimeout(function(){
    doWait(checkControlFunc, controlID,
    controlVal, callbackFunc, pollTime);}, pollTime);
}

The doWait function is recursive and exits when checkControlFunc returns true or local counter numTries exceeds global variable maxTries. So, this calls a function named clickCalculate, goes into a delay loop, pauses polling for 500 ms and calls the function checkControl with arguments of TextBox3 and 8.0000 until checkControl returns true or the delay loop has executed 20 times (specified by maxTries):

callAndWait(clickCalculate, checkControl, "TextBox3", 
  "8.0000", step2, polling);

If checkControl returns true, control is transferred to function step2. The clickCalulate function uses jQuery selection and chaining:

function clickCalculate() {
  var btn1 = $(parent.rightFrame.document).find('#Button1');
  if (btn1 == null || btn1.val() == undefined) 
    throw "Did not find btn1";
  btn1.click();
}

The main reason for defining an action wrapper function like this is so that the function can be conveniently passed by name to the callAndWait function. The checkControl function is straightforward:

function checkControl(controlID, controlVal) {
  var ctrl = $(parent.rightFrame.document).find('#' + controlID);
  if (ctrl == null || ctrl.val() == undefined || ctrl.val() == "")
    return false;
  else
    return (ctrl.val() == controlVal);
}

First I use jQuery syntax to get a reference to the control specified by parameter controlID. If the value of the control is not yet available, I immediately return to the delay loop. Once the control value is ready I can check to see whether it’s equal to some expected value given by parameter controlVal.

After calling as many stepX functions as I care to call, I transfer control to a finish function. That function first determines how it was reached:

if (numTries > maxTries) {
  logRemark("\nnumTries has exceeded maxTries");
  logRemark("\n*FAIL*");
}
else ....

If the value of the global numTries variable exceeds the value of maxTries, then I know that the Web application under test hasn’t responded within the time allowed. Here I arbitrarily decide that this is a test case failure rather than some form of an undetermined result. If numTries hasn’t exceeded maxTries I begin checking the final state of the app under test:

logRemark("\nChecking final state");
var tb1 = $(parent.rightFrame.document).find('#TextBox1');
var tb2 = $(parent.rightFrame.document).find('#TextBox2');
var tb3 = $(parent.rightFrame.document).find('#TextBox3');

Here I get references to the three textbox controls. Exactly which elements of the Web app under test you decide to check will depend on the details of your particular application. Next, I examine the value of each textbox control to see if each has an expected value:

var result = "pass";
if (tb1.val() != "3") result = "fail";
if (tb2.val() != "5") result = "fail";
if (tb3.val() != "8.0000") result = "fail";

My test scenario script has all test case input and expected values hard-coded. The test automation I present is best suited for lightweight, quick testing situations where hard-coded test data is simple and effective.

The finish function wraps up the test run by displaying a pass or fail result:

if (result == 'pass')
  logRemark("\n*Pass*");
else
  logRemark("\n*FAIL*");

As with the test case input data, this approach is lightweight and you may want to write test results to an external file on the test host or Web server, or perhaps send test results via SMTP to an e-mail address.

Wrapping Up

The harness described here is semi-automated in the sense that you must click on a button control to launch the test. You can fully automate the harness by adding a start-wrapper function:

function launch() {
  if (!started)
    runTest();
}

Add an attribute of onload=“leftFrame.launch();” to the frameset element in the harness page. Each load of the Web application in the harness will trigger an onload event, so I use the global “start” variable to prevent the test automation from restarting. Interestingly, even though the HTML Frame element doesn’t support an onload event, you can in fact place an onload attribute in the harness frame element, and the event will bubble up to its parent frameset element.

Now you can create a .bat file with commands such as:

iexplore https://localhost/TestWithJQuery/UITestHarness001.html
iexplore https://localhost/TestWithJQuery/UITestHarness002.html

When the .bat file executes—perhaps via a Windows Task 
Scheduler—the harness will load, and your automation will launch automatically. Another way you might want to extend the test system I’ve presented here is to place the program-defined functions into a jQuery plug-in.

When writing lightweight Web application UI test automation you have several alternatives to the jQuery-based approach I’ve presented here. One of the primary advantages of using the jQuery library compared to using raw JavaScript is that jQuery works across multiple browsers such as Internet Explorer, Firefox and Safari. Another significant advantage is that by using jQuery to write test automation, you can actively build your knowledge of using jQuery for Web development tasks.

Using jQuery does have disadvantages compared to alternative approaches. The use of jQuery entails an external dependency to some extent, and script-based test automation tends to be more difficult to manage than non-script test automation. Compared to using a test framework such as Selenium or Watir, writing jQuery-based automation gives you more flexibility, but you must write code at a lower level of abstraction.

As usual, I’ll remind you that no one particular test automation approach is best suited for all situations, but jQuery-based Web application UI test automation can be an efficient and effective technique in many software development scenarios.


Dr. James McCaffrey works for Volt Information Sciences Inc., where he manages technical training for software engineers working at the Microsoft Redmond, Wash., campus. He’s 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 jammc@microsoft.com.

Thanks to the following technical experts for reviewing this article: Scott Hanselman and Matthew Osborn