Ajax Control Toolkit

Automated Testing

JavaScript presents several challenges when testing user interface components written using ASP.NET AJAX. With asynchronous operations like web service calls or animation, you have to start the operation and wait for it to complete without blocking before you can verify the result. Postbacks are just as difficult because you have to perform an action that causes a postback and then have the test resume processing where it left off when the page loads again. We have written a JavaScript testing framework, in the ToolkitTests web project, to eliminate these problems when writing tests for your components.

The following walkthrough describes the steps you need to take to get started using the AJAX Control Toolkit Automated Test Harness.

Creating a Component Unit Test

The AJAX Control Toolkit Automated Test Harness allows you to easily write tests for user interface components written in JavaScript. The test harness is the web application that actually runs all of the selected test suites and displays their results. A test suite is an *.aspx page that contains instances of your component as well as the definition of several test cases. A test case is an individual unit test consisting of a series of test steps to evaluate a particular area of functionality. A test step is an action (possibly performed asynchronously or after a postback) that operates on your component.

There are four basic steps to creating a suite:

  1. Create an ASPX page with the UI that you'd like to test.
  2. Declare a top-level JavaScript variable called typeDependencies that lists the types that need to be loaded for your type to run.
  3. Declare a JavaScript function called registerTests that defines the tests and the steps of those tests.
  4. Inside of registerTests, declare a set of tests and test steps.

As you'll see below, the Test Harness takes a set of steps, queues them up, then executes them in order, either synchronously or asynchronously, by waiting for their completion function to return true.

To create a new test suite, add a new Web Form to the ToolkitTests project and select the Default.master master page. Then add instances of your component to the Web Form that you will use in the tests. Next, you need to create a <script> block that contains definitions of your test cases and test harness entry points. If you were writing a test suite for CascadingDropDown, it would look something like this:

<%@ Page
Language="C#"
CodeFile="CascadingDropDown.aspx.cs"
Inherits="Automated_CascadingDropDown"
Title="CascadingDropDown Tests"
MasterPageFile="~/Default.master"
EnableEventValidation="false" %>
<asp:Content ID="Content"<
ContentPlaceHolderID="SampleContent"
Runat="Server">
<asp:DropDownList ID="DropDownList1"
runat="server" Width="170">
<ajaxToolkit:CascadingDropDown ID="CascadingDropDown1"
runat="server"
TargetControlID="DropDownList1"
Category="Make"
PromptText="Please select a make"
ServicePath="CarsService.asmx"
ServiceMethod="GetDropDownContents" />
<asp:Label ID="Label1" runat="server"
Text="Label"></asp:Label>
<asp:Button ID="Button1" runat="server"
Text="Button" OnClick="Button1_Click" />
<script type="text/javascript">

// Define the test cases

</script>
</asp:Content>

Now, in the JavaScript block, we declare an array of strings called typeDependencies that contains the fully qualified JavaScript names of the components used in your test suite. The test harness will wait for all of these objects to have been defined before running any of your tests. This is critical; without it the tests will intermittently fail based on how long it takes to load the behaviors.

For the CascadingDropDown test suite, we have:

// Script objects that should be loaded before we run
var typeDependencies =
['AjaxControlToolkit.CascadingDropDownBehavior'];

Next, declare a function registerTests that takes a parameter called harness. The test harness will pass this function a reference to itself when loading the test suite so the suite can add new test cases. You will often want to save the reference to the test harness in a global variable because it contains useful utility functions, like assertions, that you may want to use elsewhere.

Before we start writing test cases, let's first get references to controls used in the test suite with testHarness.getElement(id) and testHarness.getObject('id'). These two methods wrap document.getElementByID and Sys.Application.findControl respectively, but also raise errors if controls aren't found and prevent your test cases from running. Since we will use the references in other areas, we make them global variables. The script for the CascadingDropDown test suite should now look like this:

<script type="text/javascript">
// Script objects that should be loaded before we run
var typeDependencies =
['AjaxControlToolkit.CascadingDropDownBehavior'];

// Test Harness
var testHarness = null;

// Controls in the test page
var drop1 = null;
var btn = null;
var label = null;

// Run the tests
function registerTests(harness) {
testHarness = harness;

// Get the client ID's from the controls on the page
drop1 = testHarness.getElement(
'ctl00_SampleContent_DropDownList1');
btn = testHarness.getElement(
'ctl00_SampleContent_Button1');
label = testHarness.getElement(
'ctl00_SampleContent_Label1');
}
</script>

For the full version of these functions, please see CascadingDropDown.aspx in the ToolkitTests project. Now that we have all the plumbing out of the way, we can define the test cases using var test = testHarness.addTest('test name'). This returns a test case object that we can add a sequence of steps to for the test harness to run. We will reload the page before running each test case, so you should expect the controls to be in their initial state for each test iteration. Be careful not to expect changes to persist from one test to another.

Once we have a test case, we can add steps to it with test.addStep and test.addPostBack. There are three main types of test steps:

  • simple steps that perform an action an return: test.addStep(someFunction)
  • steps that perform an asynchronous action and wait for it to finish: test.addStep(someFunction, isCompleteFunction, pollingInterval, timeOutTime, verifyStateFunction)
  • steps that force post-backs: test.addPostBack(postBackElement)

For an asynchronous step, here are the parameter descriptions:

  • someFunction - the function to execute
  • isCompleteFunction - a function to call to check if the action has completed (optional)
  • interval - the time to wait between calls to the check function (optional)
  • timeout - the total time to wait before failing the test (optional)
  • verifyStateFunction - a function to call to verify the state after the check function has completed (optional)

Note that the functions must take no parameters (if they need to take parameters, wrap them as mentioned below).

Here are example test cases from the registerTests function in the CascadingDropDown test suite, see below for function definitions):

// Test the initial values
var test = testHarness.addTest('Initial Values');

// Wait until the drop downs are loaded
test.addStep(function() {},
checkLoaded(drop1, drop4, drop5, drop6));
test.addStep(testInitialState);


// Select from first level
var test = testHarness.addTest('Select from first level');
test.addStep(function() {},
checkLoaded(drop1, drop4, drop5, drop6));
test.addStep(testInitialState);
test.addStep(setSelectedIndex(drop1, 3),
checkLoaded(drop1, drop2),
testValuesAfterFirstSelected);

To add a postback step, simply call test.addPostBack(element); where the element is a link, button, form, etc., that will cause a postback when clicked or submitted. When the test case is run and a postback occurs, the test harness will automatically resume processing the test on the next step after the postback when the page has reloaded. The following test case from the CascadingDropDown test suite shows an example of using a postback step:

// Values preserved on postback
var test = testHarness.addTest('Values preserved on PostBack'); test.addStep(function() {},
checkLoaded(drop1, drop4, drop5, drop6));
test.addStep(testInitialState);
test.addStep(setSelectedIndex(drop1, 3),
checkLoaded(drop1, drop2),
testValuesAfterFirstSelected);
test.addPostBack(btn);
test.addStep(empty,
checkLoaded(drop1, drop2, drop4, drop5, drop6));
test.addStep(testValuesAfterFirstSelected);

To define the test cases, we will need to provide the test steps with functions that actually operate on the component. It is very important to note that the test steps can only take functions with no parameters, so if we have a function that needs parameters, wrap it in a parameterless function (for an example, see setSelectedIndex below). These functions can use the utility functions of the test harness to make them easier to write.

The utility functions will be familiar to those using TDD and include:

  • assertTrue(condition, 'message')
  • assertFalse(condition, 'message')
  • assertEqual(objA, objB, 'message')
  • assertNotEqual(objA, objB, 'message')
  • assertNull(obj, 'message')
  • assertNotNull(obj, 'message')
  • fireEvent(element, 'eventName')

For the CascadingDropDown test suite, we could add the following test functions:

// Check if the drop down elements passed as arguments
// are loaded by seeing if they have been enabled
function checkLoaded() {
// ...
}

// Ensure the dropdown is properly enabled
function checkEnabled(dropdown) {
testHarness.assertTrue(!dropdown.disabled,
"Drop down '" + dropdown.id + "' should be enabled");
}

// ...

// Set the selected index of a drop down and

// force the onChange event to be fired
function setSelectedIndex(dropdown, index) {
return function() {
dropdown.selectedIndex = index;
testHarness.fireEvent(dropdown, 'onchange');
};
}

// Test the initial state
function testInitialState() {
// ...
}

// Ensure the last dropdowns respond after a selection
// in the first
function testValuesAfterFirstSelected() {
// ...
}

// Ensure the last dropdown responds after a selection
// in the second
function testValuesAfterSecondSelected() {
// ...
}

We can now start the test harness by viewing Default.aspx of the ToolkitTests project and select our test suite to run. For the automated CascadingDropDown.aspx test suite and tests for the rest of the toolkit controls, see the ToolkitTests project.