17.3. How to Use the Web API

17.3.1. How to Find and Query Web Objects
17.3.2. How to Use XPath
17.3.3. How to Access Web Object Properties
17.3.4. How to Call Web Object Functions
17.3.5. How to Use evalJS
17.3.6. How to Use retrieveJSObject
17.3.7. How to Use the Web Convenience API
17.3.8. How to Synchronize Web Page Loading for Testing
17.3.9. How to Handle Redirects Involving a Change of Protocol or Hostname
17.3.10. How to Test Web Elements
17.3.11. How to Do Data-driven Web Application Testing
17.3.12. How to Do Web Application Load Testing

One of Squish's most useful features is the ability to access the toolkit's API from test scripts. This gives test engineers a great deal of flexibility and allows them to test just about anything in the AUT. With Squish's Web-specific API it is possible to find and query objects, access properties and methods, and evaluate arbitrary JavaScript code in the Web-application's context. In addition, Squish provides a convenience API (see How to Use the Web Convenience API (Section 17.3.7)) that provides facilities for executing common actions on Web sites such as clicking a button or entering some text.

A variety of examples that show how to use the scripting Web API to access and test complex Web elements is given in the How to Test Web Elements (Section 17.3.10) section.

[Note]Testing with the Web Proxy Mechanism

There are two ways to work with web applications—directly, or using the web proxy mechanism introduced in Squish 4.1. For testing web applications with Safari on Mac OS X, Microsoft Internet Explorer on Windows, or Firefox/Mozilla on Unix it is best not to use the proxy mechanism since the mechanism imposes a few limitations.

For more about how to use the web proxy mechanism see Web Proxy (Section 19.4.7).

17.3.1. How to Find and Query Web Objects

Squish provides two functions—findObject and waitForObject—that return a reference to the object (HTML or DOM element), for a given qualified object name. The difference between them is that waitForObject waits for an object to become available (up to its default timeout, or up to a specified timeout), so it is usually the most convenient one to use. However, only findObject can be used on hidden objects.

See the Web Object API (Section 18.10) for full details of Squish's Web classes and methods.

There are several ways to indentify a particular Web object:

  • Multiple-property (real) names—These names consist of a list of one or more property–name/property–value pairs, separated by spaces if there is more than one, and the whole name enclosed in curly braces. Given a name of this kind, Squish will search the document's DOM tree until it finds a matching object. An example of such a name is: “{tagName='INPUT' id='r1' name='rg' form='myform' type='radio' value='Radio 1'}”.

  • Single property value—Given a particular value, Squish will search the document's DOM tree until it finds an object whose id, name or innerText property has the specified value.

  • Path—The full path to the element is given. An example of such a path is “DOCUMENT.HTML1.BODY1.FORM1.SELECT1”.

To find an object's name, you can use the Spy to introspect the Web application's document. See the How to Use the Spy (Section 17.21.3) section for details.

If we want to interact with a particular object—for example, to check its properties, or to do something to it, such as click it, we must start by getting a reference to the object.

If we use the findObject function, it will either return immediately with the object, or it will throw a catchable exception if the object isn't available. (An object might not be available because it is an AJAX object that only appears under certain conditions, or it might only appear as the result of some JavaScript code executing, and so on.) Here's a code snippet that shows how to use findObject without risking an error being thrown, by using the object.exists function:

Python
radioName = ("{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}")
if object.exists(radioName):
    radioButton = findObject(radioName)
    clickButton(radioButton)
JavaScript
var radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}";
if (object.exists(radioName)) {
    var radioButton = findObject(radioName);
    clickButton(radioButton);
}
Perl
my $radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " .
        "type='radio' value='Radio 1'}"
if (object::exists($radioName)) {
    my $radioButton = findObject($radioName);
    clickButton($radioButton);
}
Ruby
radioName = "{tagName='INPUT' id='r1' name='rg' form='myform' " +
        "type='radio' value='Radio 1'}"
if Squish::Object.exists(radioName)
    radioButton = findObject(radioName)
    clickButton(radioButton)
end
Tcl
set radioName {{tagName='INPUT' id='r1' name='rg' form='myform' \
        type='radio' value='Radio 1'}}
if {[object exists $radioName]} {
    set radioButton [findObject $radioName]
    invoke clickButton $radioButton
}

This will only click the radio button if it exists, that is, if it is accessible at the time of the object.exists call.

An alternative approach is to use the waitForObject function:

Python
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
JavaScript
var radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}");
clickButton(radioButton);
Perl
my $radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " .
        "form='myform' type='radio' value='Radio 1'}");
clickButton($radioButton);
Ruby
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
clickButton(radioButton)
Tcl
set radioButton [waitForObject {{tagName='INPUT' id='r1' name='rg' \
        form='myform' type='radio' value='Radio 1'}}]
invoke clickButton $radioButton

This will wait up to 20 seconds (or whatever the default timeout has been set to), and providing the radio button becomes accessible within that time, it is clicked.

Using the findObject and waitForObject functions in conjunction with appropriate object identifiers means that we can access all the elements in a Web document's object tree, and test their properties, and generally interact with them.

17.3.2. How to Use XPath

For every object returned by the findObject and waitForObject functions, it is possible the evaluate an XPath statement. The object on which the XPath statement is evaluated is used as the context node.

For example, to retrieve the reference to a link referring to the URL www.froglogic.com which is a child of the DIV element with the idmydiv”, we can use the following code:

Python
div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0)
JavaScript
var div = findObject("{tagName='DIV' id='mydiv'}");
var link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0);
Perl
my $div = findObject("{tagName='DIV' id='mydiv'}");
my $link = $div->evaluateXPath("A[contains(@href," .
        "'www.froglogic.com')]")->snapshotItem(0);
Ruby
div = findObject("{tagName='DIV' id='mydiv'}")
link = div.evaluateXPath("A[contains(@href," +
        "'www.froglogic.com')]").snapshotItem(0)
Tcl
set div [findObject {{tagName='DIV' id='mydiv'}}]
set link [invoke [invoke $div evaluateXPath \
        "A[contains(@href, 'www.froglogic.com')]"] snapshotItem 0]

The XPath used here says, “find all A (anchor) tags that have an href attribute, and whose value is www.froglogic.com”. We then call the snapshotItem method and ask it to retrieve the first match—it uses 0-based indexing—which is returned as an object of type HTML_Object Class (Section 18.10.28).

Each XPath query can produce a boolean (true or false), a number, a string, or a group of elements as the result. Consequently, the HTML_Object.evaluateXPath method returns an object of type HTML_XPathResult Class (Section 18.10.39) on which you can query the result of the XPath evaluation.

How to Access Table Cell Contents (Section 17.3.10.5) has an example of using the HTML_Object.evaluateXPath method to extract the contents of an HTML table's cell.

[Tip]XPath Queries

For more information about how you can create XPath queries to help produce flexible and compact test scripts, refer to documentation that specializes in this topic. For example, we recommend the XPath Tutorial from the W3Schools Online Web Tutorials website.

See also the Squish for Web tutorial Inserting Additional Verification Points (Section 8.4).

17.3.3. How to Access Web Object Properties

Using the script API it is possible to access most of the DOM properties for any HTML or DOM element in a Web application. See the Web Object API (Section 18.10) for full details of Squish's Web classes and methods.

Here is an example where we will change and query the value property of a form's text element.

Python
entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}")
entry.value = "Some new text"
test.log(entry.value)
JavaScript
var entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}");
entry.value = "Some new text";
test.log(entry.value);
Perl
my $entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}");
$entry->value = "Some new text";
test::log($entry->value);
Ruby
entry = waitForObject(
        "{tagName='INPUT' id='input' form='myform' type='text'}")
entry.value = "Some new text"
Test.log(entry.value)
Tcl
set entry [waitForObject {{tagName='INPUT' id='input' \
        form='myform' type='text'}}]
[property set $entry value "Some new text"]
test log [property get $entry value]

Squish provides similar script bindings to all of the standard DOM elements' standard properties. But it is also possible to access the properties of custom objects using the property method. For example, to check a DIV element's offset width, we can write code like this:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
test.compare(div.property("offsetWidth"), 18)
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test.compare(div.property("offsetWidth"), 18);
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
test::compare($div->property("offsetWidth"), 18);
Ruby
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
Test.compare(div.property("offsetWidth"), 18)
Tcl
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
test compare [invoke $div property "offsetWidth"] 18

Note that for hidden elements we must always use the findObject function rather than the waitForObject function.

17.3.4. How to Call Web Object Functions

In addition to properties, you can call standard DOM functions on all Web objects from test scripts, using the API described in the Web Object API (Section 18.10).

For example, to get the first child node of a DIV element, you could use the following test script which makes use of the HTML_Object.firstChild function:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
test.log(child.tagName)
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
var child = div.firstChild();
test.log(child.tagName);
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
my $child = $div->firstChild();
test::log($child->tagName);
Ruby
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
child = div.firstChild()
Test.log(child.tagName)
Tcl
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
set child [invoke $div firstChild]
test log [property get $child tagName]

Or, to get the text of the selected option from a select form element, we could use the following code:

Python
element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
test.log(option.text)
JavaScript
var element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
var option = element.optionAt(element.selectedIndex);
test.log(option.text);
Perl
my $element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}");
my $option = $element->optionAt($element->selectedIndex);
test::log($option->text);
Ruby
element = findObject(
        ":{tagName='INPUT' id='sel' form='myform' type='select-one'}")
option = element.optionAt(element.selectedIndex)
Test.log(option.text)
Tcl
set element [findObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select-one'}"]
set option [invoke $element optionAt [property get element selectedIndex]]
test log [property get $option text]

Squish provides script bindings like those shown here to all the standard DOM elements' standard functions. And in addition, it is also possible to call custom functions via a generic invoke function. For example, to call a custom customFunction function with string argument “an argument”, on a DIV element, we could write code like this:

Python
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("customFunction", "an argument")
JavaScript
var div = findObject("DOCUMENT.HTML1.BODY1......DIV");
div.invoke("customFunction", "an argument");
Perl
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV");
$div->invoke("customFunction", "an argument");
Ruby
div = findObject("DOCUMENT.HTML1.BODY1......DIV")
div.invoke("customFunction", "an argument")
Tcl
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"]
invoke $div "customFunction" "an argument"

Beyond the DOM API bindings and the invoke function, Squish offers a Browser object which can be used by test scripts to query which browser is being used, as the following Python snippet shows:

# This will print out the name of the browser:
test.log("We are running in " + Browser.name())
if Browser.id() == InternetExplorer:
    ...
elif Browser.id() == Mozilla:
    ...
elif Browser.id() == Firefox:
    ...
elif Browser.id() == Safari:
    ...

17.3.5. How to Use evalJS

In addition to test scripts being able to access all the properties and methods of DOM elements, it is also possible to let Squish execute arbitrary JavaScript code in the Web browser's JavaScript interpreter and to retrieve the results. For this purpose, Squish provides the evalJS function. Here is an example of its use:

Python
style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''")
JavaScript
var style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''");
Perl
my $style_display = evalJS("var d = document.getElementById(" .
        "'busyDIV'); d ? d.style.display : ''");
Ruby
style_display = evalJS("var d = document.getElementById(" +
        "'busyDIV'); d ? d.style.display : ''")
Tcl
set style_display [invoke evalJS "var d = document.getElementById(\
        'busyDIV'); d ? d.style.display : ''"]

The evalJS function returns the result of the last statement executed—in this case the last statement is d ? d.style.display : '' so if the document contains an element with ID “busyDIV”, style_display will be set to that element's style.display property's value—otherwise it will be set to an empty string.

17.3.6. How to Use retrieveJSObject

In addition to test scripts being able to run some javascript snippet retrieve the string value of the result, Squish also allows to retrieve references to the actual javascript objects from the Web browser's interpreter. This is useful in cases where a javascript function does not return a simple value, like a string or number, but rather return an object itself. In such a case, the reference allows to again retrieve properties from that object or call methods on that object. For this purpose, Squish provides the retrieveJSObject function. Here is an example of its use:

Python
jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; };globalObject;")
JavaScript
var jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; };globalObject;");
Perl
my $jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; };globalObject;");
Ruby
jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; };globalObject;")
Tcl
set jsobject [invoke retrieveJSObject {var globalObject = \{ 'id': 'obj1', 'name': function() \{ return 'name1'; \};globalObject;\} }]

The retrieveJSObject function returns the result of the last statement executed—in this case the last statement is globalObject; so a reference to the just created globalObject is returned. Now its possible to fetch the id property of that object or call the name function. Here is an example of logging both in the test results

Python
test.log("id: " + jsobject.property("id"))
test.log("name: " + jsobject.call("name"))
JavaScript
test.log("id: " + jsobject.property("id"));
test.log("name: " + jsobject.call("name"));
Perl
test::log("id: " . $jsobject->property("id"));
test::log("name: " . $jsobject->call("name"));
Ruby
test.log("id: " + jsobject.property("id"))
test.log("name: " + jsobject.call("name"))
Tcl
test log "id: " [invoke $jsobject property "id"]
test log "name: " [invoke $jsobject call "name"]

17.3.7. How to Use the Web Convenience API

This section describes the script API Squish offers on top of the DOM API to make it easy to perform common user actions such as clicking a link, entering text, etc. All the functions provided by the API are listed in the Web Object API (Section 18.10) section in the Tools Reference Manual (Chapter 19). Here we will show a few examples to illustrate how the API is used.

In the example below, we click a link, select an option, and enter some text.

Python
clickLink(":{tagName='A' innerText='Advanced Search'}")
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana")
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text")
JavaScript
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text");
Perl
clickLink(":{tagName='A' innerText='Advanced Search'}");
selectOption(":{tagName='INPUT' id='sel' form='myform' " .
        "type='select-one'}", "Banana");
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text");
Ruby
clickLink(":{tagName='A' innerText='Advanced Search'}")
selectOption(":{tagName='INPUT' id='sel' form='myform' " +
        "type='select-one'}", "Banana")
setText(":{tagName='INPUT' id='input' form='myform' type='text'}",
        "Some Text")
Tcl
invoke clickLink ":{tagName='A' innerText='Advanced Search'}"
invoke selectOption ":{tagName='INPUT' id='sel' form='myform' \
        type='select-one'}" "Banana"
invoke setText ":{tagName='INPUT' id='input' form='myform' \
        type='text'}" "Some Text"

In these cases we identified the object using real (multi-property) names; we could just have easily used symbolic names, or even object references, instead. Note also that the full API contains far more functions than the three mentioned here (clickLink, selectOption, and setText), although all of them are just as easy to use.

17.3.8. How to Synchronize Web Page Loading for Testing

In many simple cases, just waiting for a particular object to become available using the waitForObject function is sufficient.

However, in some cases we need to ensure that the page has loaded before we attempt to access its objects. The special isPageLoaded function makes it possible to synchronize a test script with a Web application's page loaded status.

We could use this function to wait for a Web page to be fully loaded before clicking a particular button on the page. For example, if a page has a Login button, we could ensure that the page is loaded before attempting to click the button, using the following code:

Python
loaded = waitFor("isPageLoaded()", 5000)
if loaded:
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"))
else:
    test.fatal("Page loading failed")
JavaScript
var loaded = waitFor("isPageLoaded()", 5000);
if (loaded)
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
else
    test.fatal("Page loading failed");
Perl
my $loaded = waitFor("isPageLoaded()", 5000);
if ($loaded) {
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"));
}
else {
    test::fatal("Page loading failed");
}
Ruby
loaded = waitFor("isPageLoaded", 5000)
if loaded
    clickButton(waitForObject(
        ":{tagName='INPUT' type='button' value='Login'}"))
else
    Test.fatal("Page loading failed")
end
Tcl
set loaded [waitFor {invoke isPageLoaded} 5000]
if {$loaded} {
    invoke clickButton [invoke waitForObject \
        ":{tagName='INPUT' type='button' value='Login'}"]
} else {
    test fatal "Page loading failed"
}

It is necessary to use the isPageLoaded function to ensure that the page is loaded and its web objects are potentially accessible. To access a particular object we must still use the waitForObject function—and we may even have to specify a longer timeout than the default 20 000 milliseconds to allow for network latency.

17.3.9. How to Handle Redirects Involving a Change of Protocol or Hostname

[Note]Squish for Web—Windows and Microsoft Internet Explorer-specific

This section describes a limitation and workaround that only applies to Squish for Web with Microsoft Internet Explorer.

When using Squish for Web with Microsoft Internet Explorer you might encounter a problem with web sites that automatically redirect their users to a URL that has a different protocol or hostname to the original URL. For example, if you were recording on the site http://www.example.com and completed a login form that redirected to either https://www.example.com (i.e., a site with a different protocol), or to http://www.example-secured.com (i.e., a site with a different hostname), recording and subsequent replaying would fail.

The reason for this limitation is that some parts of Squish run inside the browser itself and for Squish to communicate amongst its components it uses a technique called “cross-site-scripting”. As a security measure, Internet Explorer disallows this kind of communication—unless the source URL (i.e., the web site being interacted with) is in Microsoft Internet Explorer's list of trusted sites.

In the case of a change of protocol the easiest solution is to change the URL that the loadUrl function uses so that it doesn't mention the protocol. For example, change "http://www.example.com" to "www.example.com". This should work in most cases since Internet Explorer uses wildcard matching for its trusted sites, so if it has an entry for *.example.com, any URL whose site name ends in that text will match—for example both http://www.example.com and https://secure.example.com will match.

In the case of a hostname that changes the easiest solution is to add the other hostname to Microsoft Internet Explorer's list of trusted sites. Alternatively, manually add a suitable call to the loadUrl function with the relevant hostname when it is needed.

17.3.10. How to Test Web Elements

In this section we will cover how to test specific HTML elements in a Web application. This will allow us to verify that elements have properties with the values we expect and that form elements have their expected contents.

One aspect of testing that can be quite challenging is the creation of test verifications. As shown in the section Inserting Additional Verification Points (Section 8.4) in Tutorial: Starting to Test Web Applications (Chapter 8), most of this can be done using the Spy and its point & click interface. But in some cases it is actually more convenient—and more flexibile—to implement verification points directly in code.

To test and verify a widget and its properties or contents in code, first we need access to the widget in the test script. To obtain a reference to the widget, the waitForObject function is used. This function finds the widget with the given name and returns a reference to it. For this purpose we need to know the name of the widget we want to test, and we can get the name using the Spy tool (see How to Use the Spy (Section 17.21.3)) and adding the object to the Object Map (Section 19.10) (so that Squish will remember it) and then copying the object's name (preferably its symbolic name) to the clipboard ready to be pasted into our test. If we need to gather the names of lots of widgets it is probably faster and easier to record a dummy test during which we make sure that we access every widget we want to verify in our manually written test script. This will cause Squish to add all the relevant names to the Object Map (Section 19.10), which we can then copy and paste into our code.

17.3.10.1. How to Test the State of Web Elements

One of the most common test requirements is to verify that a particular element is enabled or disabled at some point during the test run. This verification is easily made by checking an element's disabled property.

Python
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
test.verify(not entry.disabled)
JavaScript
var entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}");
test.verify(!entry.disabled);
Perl
my $entry = waitForObject("{tagName='INPUT' id='input' " .
        "form='myform' type='text'}");
test::verify(!$entry->disabled);
Ruby
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
Test.verify(!entry.disabled)
Tcl
set entry [waitForObject "{tagName='INPUT' id='input' \
        form='myform' type='text'}"]
test verify [expr ![property get $entry disabled]]

Here we have verified that a text entry element is enabled (i.e., that its disabled property is false). To check that the element is disabled, we would eliminate the negation (not or ! depending on language).

17.3.10.2. Form Checkboxes and Radiobuttons

To verify that a radiobutton or checkbox is checked, we just need to query its checked property.

Python
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
test.verify(radiobutton.checked)
JavaScript
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}");
test.verify(radiobutton.checked);
Perl
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " .
        "form='myform' type='radio' value='Radio 1'}");
test::verify($radiobutton->checked);
Ruby
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " +
        "form='myform' type='radio' value='Radio 1'}")
Test.verify(radiobutton.checked)
Tcl
set radiobutton [waitForObject ":{tagName='INPUT' id='r1' name='rg' \
        form='myform' type='radio' value='Radio 1'}"]
test verify [property get $radiobutton checked]

The coding pattern shown here—get a reference to an object, then verify the value of one of its properties—is very common and can be applied to any element.

17.3.10.3. Form Text fields

Both the text and textarea form elements have a value property, so it is easy to check what they contain.

Python
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
test.compare(entry.value, "Ternary")
JavaScript
var entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}");
test.compare(entry.value, "Ternary");
Perl
my $entry = waitForObject("{tagName='INPUT' id='input' " .
        "form='myform' type='text'}");
test::compare($entry->value, "Ternary");
Ruby
entry = waitForObject("{tagName='INPUT' id='input' " +
        "form='myform' type='text'}")
Test.compare(entry.value, "Ternary")
Tcl
set entry [waitForObject "{tagName='INPUT' id='input' \
        form='myform' type='text'}"]
test compare [property get $entry value] "Ternary"

This follows exactly the same pattern as we used for the earlier examples.

17.3.10.4. Form Selection Boxes

Web forms usually present single selection lists (of element type select-one) in comboboxes and multiple selection lists (of element type select) in listboxes. We can easily check which items are selected.

Python
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}")
test.compare(selection.selectedIndex, 2)
test.compare(selection.selectedOption, "Cavalier")
JavaScript
var selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}");
test.compare(selection.selectedIndex, 2);
test.compare(selection.selectedOption, "Cavalier");
Perl
my $selection = waitForObject(":{tagName='INPUT' id='sel' " .
        "form='myform' type='select-one'}");
test::compare($selection->selectedIndex, 2);
test::compare($selection->selectedOption, "Cavalier");
Ruby
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select-one'}")
Test.compare(selection.selectedIndex, 2)
Test.compare(selection.selectedOption, "Cavalier")
Tcl
set selection [waitForObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select-one'}"]
test compare [property get $selection selectedIndex] 2
test compare [property get $selection selectedOption] "Cavalier"

Here we retrieve the selected item from a single selection list box and verify that the third item (the item at index position 2), is selected, and that it has the text “Cavalier”.

Python
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}")
test.verify(selection.optionAt(0).selected)
test.verify(not selection.optionAt(1).selected)
test.verify(selection.optionAt(2).selected)
test.compare(selection.optionAt(1).text, "Round Head")
JavaScript
var selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}");
test.verify(selection.optionAt(0).selected);
test.verify(!selection.optionAt(1).selected);
test.verify(selection.optionAt(2).selected);
test.compare(selection.optionAt(1).text, "Round Head");
Perl
my $selection = waitForObject(":{tagName='INPUT' id='sel' " .
        "form='myform' type='select'}");
test::verify($selection->optionAt(0)->selected);
test::verify(!$selection->optionAt(1)->selected);
test::verify($selection->optionAt(2)->selected);
test::compare($selection->optionAt(1)->text, "Round Head");
Ruby
selection = waitForObject(":{tagName='INPUT' id='sel' " +
        "form='myform' type='select'}")
Test.verify(selection.optionAt(0).selected)
Test.verify(!selection.optionAt(1).selected)
Test.verify(selection.optionAt(2).selected)
Test.compare(selection.optionAt(1).text, "Round Head")
Tcl
set selection [waitForObject ":{tagName='INPUT' id='sel' \
        form='myform' type='select'}"]
test verify [property get [invoke selection optionAt 0] selected]
test verify [expr ![property get [invoke selection optionAt 1] selected]]
test verify [property get [invoke selection optionAt 2] selected]
test.compare [property get [invoke selection optionAt 1] text] \
        "Round Head"

In this example, we retrieve a reference to a mulitple selection list—normally represented by a listbox—and then retrieve its option items. We then verify that the first option (at index position 0) is selected, that the second option (at index position 1) is not selected, and that the third option (at index position 2) is selected. We also verify the second option's text is “Round Head”.

See also the HTML_Select Class (Section 18.10.31) class, its HTML_Select.optionAt function, and its text and selected properties.

17.3.10.5. How to Access Table Cell Contents

Another common requirement when testing Web applications is to retrieve the text contents of particular cells in HTML tables. This is actually very easy to do with Squish.

All HTML elements retrieved with the findObject function and the waitForObject function have an HTML_Object.evaluateXPath method that can be used to query the HTML element, and which returns the results of the query. We can make use of this to create a generic custom getCellText function that will do the job we want. Here's an example implementation:

Python
def getCellText(tableObject, row, column):
    return tableObject.evaluateXPath("TBODY/TR[%d]/TD[%d]" % (
        row + 1, column + 1)).stringValue
JavaScript
function getCellText(tableObject, row, column)
{
    return tableObject.evaluateXPath("TBODY/TR[" + (row + 1) +
        "]/TD[" + (column + 1) + "]").stringValue;
}
Perl
sub getCellText
{
    my ($tableObject, $row, $column) = @_;
    ++$row;
    ++$column;
    return $tableObject->evaluateXPath(
        "TBODY/TR[$row]/TD[$column]")->stringValue;
}
Ruby
def getCellText(tableObject, row, column)
    tableObject.evaluateXPath(
        "TBODY/TR[#{row + 1}]/TD[#{column + 1}]").stringValue
end
Tcl
proc getCellText {tableObject row column} {
    incr row
    incr column
    set argument "TBODY/TR[$row]/TD[$column]"
    return [property get [invoke $tableObject \
        evaluateXPath $argument] stringValue]
}

An XPath is kind of like a file path in that each component is separated by a /. The XPath used here says, “find every TBODY tag, and inside each one find the row-th TR tag, and inside that find the column-th TD tag”. The result is always an object of type HTML_XPathResult Class (Section 18.10.39); here we return the result query as a single string value using the result's stringValue property. (So if there was more than one TBODY tag in the document that had a cell at the row and column we wanted, we'd actually get the text of all of them.) We must add 1 to the row and to the column because XPath queries use 1-based indexing, but we prefer our functions to have 0-based indexing since that is the kind used by all the scripting languages that Squish supports. The function can be used like this:

Python
table = waitForObject(htmlTableName)
text = getCellText(table, 23, 11)
JavaScript
var table = waitForObject(htmlTableName);
var text = getCellText(table, 23, 11);
Perl
my $table = waitForObject($htmlTableName);
my $text = getCellText($table, 23, 11);
Ruby
table = waitForObject(htmlTableName)
text = getCellText(table, 23, 11)
Tcl
set table [waitForObject $htmlTableName]
set text [getCellText $table 23 11]

This code will return the text from the cell at the 22nd row and 10th column of the HTML table whose name is in the htmlTableName variable.

Squish's XPath functionality is covered in How to Use XPath (Section 17.3.2).

17.3.10.6. Non-Form Elements and Synchronization

Of course it is also possible to verify the states and contents of any other element in a Web application's DOM tree.

For example, we might want to verify that a table with the ID result_table contains the text—somewhere in the table, we don't care where—“Total: 387.92”.

Python
table = waitForObject("{tagName='TABLE' id='result_table]'}")
contents = table.innerText
test.verify(contents.find("Total: 387.92") != -1)
JavaScript
var table = waitForObject("{tagName='TABLE' id='result_table]'}");
var contents = table.innerText;
test.verify(contents.indexOf("Total: 387.92") != -1);
Perl
my $table = waitForObject("{tagName='TABLE' id='result_table]'}");
my $contents = $table->innerText;
test::verify(index($contents, "Total: 387.92") != -1);
Ruby
table = waitForObject("{tagName='TABLE' id='result_table]'}")
contents = table.innerText
Test.verify(contents.find("Total: 387.92") != -1)
Tcl
set table [waitForObject "{tagName='TABLE' id='result_table]'}"]
set contents [property get $table innerText]
test verify [expr [string first "Total: 387.92" $contents] != -1]

The innerText property gives us the entire table's text as a string, so we can easily search it.

Here's another example, this time checking that a DIV tag with the ID syncDIV is hidden.

Python
div = waitForObject(":{tagName='DIV' id='syncDIV'}")
test.compare(div.style().value("display"), "hidden")
JavaScript
var div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test.compare(div.style().value("display"), "hidden");
Perl
my $div = waitForObject(":{tagName='DIV' id='syncDIV'}");
test::compare($div->style()->value("display"), "hidden");
Ruby
div = waitForObject(":{tagName='DIV' id='syncDIV'}")
Test.compare(div.style().value("display"), "hidden")
Tcl
set div [waitForObject ":{tagName='DIV' id='syncDIV'}"]
test compare [invoke $div style [invoke value "display"]] "hidden"

Notice that we must use the HTML_Object.style function (rather than writing, say div.style.display).

Often such DIV elements are used for synchronization. For example, after a new page is loaded, we might want to wait until a particular DIV element exists and is hidden—perhaps some JavaScript code in the HTML page hides the DIV, so when the DIV is hidden we know that the browser is ready because the JavaScript has been executed.

Python
def isDIVReady(name):
    if not object.exists(":{tagName='DIV' id='%s'}" % name):
       return False
    return waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden"

# later on...
waitFor("isDIVReady('syncDIV')")
JavaScript
function isDIVReady(name)
{
    if (!object.exists(":{tagName='DIV' id='" + name + "'}"))
       return false;
    return waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden";
}

// later on...
waitFor("isDIVReady('syncDIV')");
Perl
sub isDIVReady
{
    my $name = shift @_;
    if (!object::exists(":{tagName='DIV' id='$name'}")) {
       return 0;
    }
     return waitForObject(":{tagName='DIV' id='syncDIV'}")->style()->value(
        "display") eq "hidden";
}

# later on...
waitFor("isDIVReady('syncDIV')");
Ruby
def isDIVReady(name)
    if !Squish::Object.exists(":{tagName='DIV' id='#{name}'}")
       return false
    end
    waitForObject(":{tagName='DIV' id='syncDIV'}").style().value(
        "display") == "hidden"
end

# later on...
waitFor("isDIVReady('syncDIV')")
Tcl
proc isDIVReady {name} {
    if {![object exists ":{tagName='DIV' id='${name}'}"]} {
       return false
    }
    set div [waitForObject ":{tagName='DIV' id='syncDIV'}"]
    set display [invoke $div style [invoke value "display"]]
    return [string equal $display "hidden"]
}

# later on...
[waitFor {isDIVReady('syncDIV')}]

We can easily use the waitFor function to make Squish wait for the code we give it to execute to complete. (Although it is designed for things that won't take too long.)

17.3.11. How to Do Data-driven Web Application Testing

This tutorial takes you through the process of migrating a pre-existing test script into a data-driven one. It expects you to know how to set up simple test scripts and how to use the Squish IDE for recording, editing, and running test scripts. Creating whole test cases from scratch is not covered here, so if you're just starting out using Squish for Web for web testing you are better off starting with the tutorial (Tutorial: Starting to Test Web Applications (Chapter 8)).

After briefly explaining the test script that this tutorial is based on (Starting with an Ordinary Test Script (Section 17.3.11.1)), we will set up some test data in Setting up the Test Data (Section 17.3.11.2) and then integrate the test data into our test script in Making a Test Script Data Driven (Section 17.3.11.3). The complete JavaScript version of the test script can be found in squish/examples/web/suite_examples/tst_icefaces_addressbook_datadriven.

17.3.11.1. Starting with an Ordinary Test Script

Our starting point for this tutorial will be the following test script:

Python
def main():
    loadUrl(":http://address.icefaces.org/address/")
    # wait for the first entry object to become available
    waitForObject(":_id0:title_select-one")
    # check that the submit button is disabled
    test.compare(findObject(":_id0:Submit_image").disabled, True)
    # enter some data
    selectOption(":_id0:title_select-one", "Mr.")
    setText(":_id0:firstName_text", "Reginald")
    setText(":_id0:lastName_text", "Stadlbauer")
    setText(":_id0:city_text", "San Francisco")
    # check that after entering a city, the state is automatically and
    # correctly chosen
    setFocus(":_id0:lastName_text")
    if not test.verify(waitFor(
        "findObject(':_id0:state_text').value == 'CA'", 10000)):
        closeWindow(":[Window]")
        return
    # input ZIP
    selectOption(":_id0:zipSelect_select-one", "94103")
    # check that the submit button is now enabled
    setFocus(":_id0:lastName_text")
    if not test.verify(waitFor(
        "findObject(':_id0:Submit_image').disabled == false", 10000)):
        closeWindow(":[Window]")
        return
    # submit
    clickButton(":_id0:Submit_image")
    # wait for results page
    waitForContextExists(":response.iface")
    waitForObject(":_id1:_id3_SPAN")
    # verify that data is stored and displayed correctly
    test.compare(findObject(":_id1:_id3_SPAN").innerText, "Reginald")
    test.compare(findObject(":_id1:_id6_SPAN").innerText, "CA")
    # close browser
    closeWindow(":[Window]")
JavaScript
function main()
{
    loadUrl(":http://address.icefaces.org/address/");
    // wait for the first entry object to become available
    waitForObject(":_id0:title_select-one");
    // check that the submit button is disabled
    test.compare(findObject(":_id0:Submit_image").disabled, true);
    // enter some data
    selectOption(":_id0:title_select-one", "Mr.");
    setText(":_id0:firstName_text", "Reginald");
    setText(":_id0:lastName_text", "Stadlbauer");
    setText(":_id0:city_text", "San Francisco");
    // check that after entering a city, the state is automatically and
    // correctly chosen
    setFocus(":_id0:lastName_text");  
    if (!test.verify(waitFor(
        "findObject(':_id0:state_text').value == 'CA'", 10000))) {
        closeWindow(":[Window]");
        return;
    }
    // input ZIP
    selectOption(":_id0:zipSelect_select-one", "94103");
    // check that the submit button is now enabled
    setFocus(":_id0:lastName_text");
    if (!test.verify(waitFor(
        "findObject(':_id0:Submit_image').disabled == false", 10000))) {
        closeWindow(":[Window]");
        return;
    }
    // submit
    clickButton(":_id0:Submit_image");
    // wait for results page
    waitForContextExists(":response.iface");
    waitForObject(":_id1:_id3_SPAN");
    // verify that data is stored and displayed correctly
    test.compare(findObject(":_id1:_id3_SPAN").innerText, "Reginald");
    test.compare(findObject(":_id1:_id6_SPAN").innerText, "CA");
    // close browser
    closeWindow(":[Window]");
}
Perl
sub main
{
    loadUrl(":http:#address.icefaces.org/address/");
    # wait for the first entry object to become available
    waitForObject(":_id0:title_select-one");
    # check that the submit button is disabled
    test::compare(findObject(":_id0:Submit_image")->disabled, 1);
    # enter some data
    selectOption(":_id0:title_select-one", "Mr.");
    setText(":_id0:firstName_text", "Reginald");
    setText(":_id0:lastName_text", "Stadlbauer");
    setText(":_id0:city_text", "San Francisco");
    # check that after entering a city, the state is automatically and
    # correctly chosen
    setFocus(":_id0:lastName_text");  
    if (!test::verify(waitFor(
        "findObject(':_id0:state_text')->value eq 'CA'", 10000))) {
        closeWindow(":[Window]");
        return;
    }
    # input ZIP
    selectOption(":_id0:zipSelect_select-one", "94103");
    # check that the submit button is now enabled
    setFocus(":_id0:lastName_text");
    if (!test::verify(waitFor(
        "findObject(':_id0:Submit_image')->disabled == 0", 10000))) {
        closeWindow(":[Window]");
        return;
    }
    # submit
    clickButton(":_id0:Submit_image");
    # wait for results page
    waitForContextExists(":response.iface");
    waitForObject(":_id1:_id3_SPAN");
    # verify that data is stored and displayed correctly
    test::compare(findObject(":_id1:_id3_SPAN")->innerText, "Reginald");
    test::compare(findObject(":_id1:_id6_SPAN")->innerText, "CA");
    # close browser
    closeWindow(":[Window]");
}
Ruby
# encoding: UTF-8
require 'squish'
include Squish

def main
    loadUrl(":http://address.icefaces.org/address/")
    # wait for the first entry object to become available
    waitForObject(":_id0:title_select-one")
    # check that the submit button is disabled
    Test.compare(findObject(":_id0:Submit_image").disabled, true)
    # enter some data
    selectOption(":_id0:title_select-one", "Mr.")
    setText(":_id0:firstName_text", "Reginald")
    setText(":_id0:lastName_text", "Stadlbauer")
    setText(":_id0:city_text", "San Francisco")
    # check that after entering a city, the state is automatically and
    # correctly chosen
    setFocus(":_id0:lastName_text")
    if !Test.verify(waitFor(
        "findObject(':_id0:state_text').value == 'CA'", 10000))
        closeWindow(":[Window]")
        return
    end
    # input ZIP
    selectOption(":_id0:zipSelect_select-one", "94103")
    # check that the submit button is now enabled
    setFocus(":_id0:lastName_text")
    if !Test.verify(waitFor(
        "findObject(':_id0:Submit_image').disabled == false", 10000))
        closeWindow(":[Window]")
        return
    end
    # submit
    clickButton(":_id0:Submit_image")
    # wait for results page
    waitForContextExists(":response.iface")
    waitForObject(":_id1:_id3_SPAN")
    # verify that data is stored and displayed correctly
    Test.compare(findObject(":_id1:_id3_SPAN").innerText, "Reginald")
    Test.compare(findObject(":_id1:_id6_SPAN").innerText, "CA")
    # close browser
    closeWindow(":[Window]")
end
Tcl
proc main {} {
    invoke loadUrl ":http://address.icefaces.org/address/"
    # wait for the first entry object to become available
    [waitForObject ":_id0:title_select-one"]
    # check that the submit button is disabled
    test compare [property get [findObject ":_id0:Submit_image"] \
        disabled] true
    # enter some data
    invoke selectOption ":_id0:title_select-one" "Mr."
    invoke setText ":_id0:firstName_text" "Reginald"
    invoke setText ":_id0:lastName_text" "Stadlbauer"
    invoke setText ":_id0:city_text" "San Francisco"
    # check that after entering a city, the state is automatically and
    # correctly chosen
    setFocus ":_id0:lastName_text" 
    set widget [waitForObject ":_id0:state_text" 10000]
    if {[property get $widget value] == "CA"} {
        invoke closeWindow ":[Window]"
        return
    }
    # input ZIP
    invoke selectOption ":_id0:zipSelect_select-one" "94103"
    # check that the submit button is now enabled
    setFocus ":_id0:lastName_text"
    set widget [waitForObject ":_id0:Submit_image" 10000]
    if {[property get $widget disabled] == false} {
        invoke closeWindow ":[Window]"
        return
    }
    # submit
    invoke clickButton ":_id0:Submit_image"
    # wait for results page
    invoke waitForContextExists ":response.iface"
    [waitForObject ":_id1:_id3_SPAN"]
    # verify that data is stored and displayed correctly
    test compare [property get \
        [findObject ":_id1:_id3_SPAN"] innerText] "Reginald"
    test compare [property get \
        [findObject ":_id1:_id6_SPAN"] innerText] "CA"
    # close browser
    invoke closeWindow ":[Window]"
}

This test script goes to http://address.icefaces.org/address/, a web page with an address form example. The test waits until the page has been loaded completely, and then enters some data into the form's fields and presses the submit button. At various points some of the web application's states are verified. For example, the test script checks the submit buttons' state in two different places, and after entering a city it checks whether the web application chooses the correct state. In addition, once the the form has been submitted, the test script checks whether the web application correctly received the data that was entered and which is displayed on the page that appears after the submit is complete.

This test script is fully functional, but it only tests the web application with a single address. If we want the address form to be tested using multiple addresses, we must adapt our test script. One obvious approach would be to duplicate the code as many times as necessary but changing the addresses in each copy. This is easy to do, but extremly difficult to maintain, for example, if we wanted to make a change in the test to correspond with some change in the web application we'd have to change every copy of the code. Another approach is to put most of the code into a separate function, passing the address details as parameters, and calling the function in a loop that reads addresses from an array of addresses. This would work fine and also be perfectly maintainable, but it still means that we must mix the data with the code. What would be better is if we could separate the data and have the test script operate on whatever address data we made available. This would allow us to change the data, for example, adding to it, without having to change the test script at all. This is the essence of data-driven testing, and we will see how to do it in the following sections.

17.3.11.2. Setting up the Test Data

The first step in migrating the test script into a data-driven one is to set up some test data.

We have chosen to store the test data in a file with the format of one record per line, and fields separated by tabs. The file's first line is not a data line, but instead holds the column captions. Here's what a tiny example data file might look like (with the → symbol used to signify tabs):

Title→FirstName→LastName→City→State→ZIP
Mr.→Reginald→Stadlbauer→San Francisco→CA→94103
Mrs.→Tanja→Rondi→New York→NY→10004

Data files can either be created separately, for example, using a spreadsheet or even a plain text editor, or using Squish's built-in functionality (which is what we'll use for this example). [20]

We can create a test data file using the Squish IDE. Test data files can be added to a single test case, or—for data that we want several test cases to use—added to the test suite. To do either, click the test case or the test suite to select it, and then choose File|New...|Squish Test Data File from the menu (or right click the test case or test suite and click New...|Squish Test Data from the context menu). This will result in the New Squish Test Data dialog (Section 20.3.7) appearing. We can enter the name of the test data file and then create the test data in the Squish IDE by adding columns, renaming columns, and adding rows of data. We'll call the test data file, addresses.tsv; once it has been created it will be opened automatically ready to be populated with data.

Our test script uses six values (Title, First Name, Last Name, City, State, and ZIP code) to represent one address. So we will create six columns, one for each of these. And since the column names will be used in the test script it is best to give them simple names—here, we'll call them "Title", "FirstName", "LastName", "City", "State" and "ZIP".

With the columns created and named we can now populate the test data file with data. Enter whatever names and addresses you like, or use those shown above.

17.3.11.3. Making a Test Script Data Driven

Now we must adapt our original test script to make use of our test data rather than using the single hard-coded address it has at present. This is done by making the test script open the test data and iterate over every record (i.e., address) in the test data, and for each record performing the operations we performed on the one original address on each of the addresses in the test data. We can either do this by changing our code manually, or by asking Squish to apply the necessary changes for us.

If we want Squish to do the changes, once the test data has been created, we must go to the editor that has the code and select the code that we want to make data driven. With the code selected we then right-click to pop up the context menu and choose the Make Script Code Data-Driven... menu option. In this example, we must select almost all of the lines in the file, starting with the waitForObject(":_id0:title_select-one") statement up to and including the clickButton(":_id1:Return_image") call. In response, Squish will pop up a dialog from which you can choose which test data file you want to use. After clicking OK in the dialog, the start of your script should now look something like this:

Python
def main():
    # open the URL
    loadUrl(":http://address.icefaces.org/address/")
    for record in testData.dataset("addresses.tsv"):
        # read record fields
        title = testData.field(record, "Title")
        firstName = testData.field(record, "FirstName")
        lastName = testData.field(record, "LastName")
        city = testData.field(record, "City")
        state = testData.field(record, "State")
        zip = testData.field(record, "ZIP")
        
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one")
        
        # check that the submit button is disabled
        test.compare(findObject(":_id0:Submit_image").disabled, True)
JavaScript
function main()
{
    // open the URL
    loadUrl(":http://address.icefaces.org/address/");
    var address_set = testData.dataset("addresses.tsv");
    
    for (var index in address_set) {
        // read record fields
        var title = testData.field(address_set[index], "Title");
        var firstName = testData.field(address_set[index], "FirstName");
        var lastName = testData.field(address_set[index], "LastName");
        var city = testData.field(address_set[index], "City");
        var state = testData.field(address_set[index], "State");
        var zip = testData.field(address_set[index], "ZIP");
        
        // wait for the first entry object to be available
        waitForObject(":_id0:title_select-one");
        
        // check that the submit button is disabled
        test.compare(findObject(":_id0:Submit_image").disabled, true);
Perl
sub main
{
    # open the URL
    loadUrl(":http://address.icefaces.org/address/");
    my @address_set = testData::dataset("addresses.tsv");
    
    foreach (@address_set) {
        # read record fields
        my $record = $_;
        my $title = testData::field($record, "Title");
        my $firstName = testData::field($record, "FirstName");
        my $lastName = testData::field($record, "LastName");
        my $city = testData::field($record, "City");
        my $state = testData::field($record, "State");
        my $zip = testData::field($record, "ZIP");
        
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one");
        
        # check that the submit button is disabled
        test::compare(findObject(":_id0:Submit_image")->disabled, 1);
Ruby
# encoding: UTF-8
require 'squish'
include Squish

def main
    # open the URL
    loadUrl(":http://address.icefaces.org/address/")
    TestData.dataset("addresses.tsv").each do |record|
        # read record fields
        title = TestData.field(record, "Title")
        firstName = TestData.field(record, "FirstName")
        lastName = TestData.field(record, "LastName")
        city = TestData.field(record, "City")
        state = TestData.field(record, "State")
        zip = TestData.field(record, "ZIP")
        
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one")
        
        # check that the submit button is disabled
        Test.compare(findObject(":_id0:Submit_image").disabled, true)
Tcl
proc main {} {
    # open the URL
    invoke loadUrl ":http://address.icefaces.org/address/"
    set data [testData dataset "addresses.tsv"
    set row 0
    for {} {$row < [llength $data]} {incr row} {
        # read record fields
        set record [lindex $data $row]
        set title [testData field $record "Title"]
        set firstName [testData field $record "FirstName"]
        set lastName [testData field $record "LastName"]
        set city [testData field $record "City"]
        set state [testData field $record "State"]
        set zip [testData field $record "ZIP"]
        
        # wait for the first entry object to be available
        [waitForObject ":_id0:title_select-one"]
        
        # check that the submit button is disabled
        test compare [property get \
            [findObject ":_id0:Submit_image"] disabled] true

The inserted code uses Squishs testData.dataset function to read the test data file. Afterwards it loops through all the records in the test data and for each one uses the testData.field function to retrieve the data. The column caption can be used to identify the individual fields. Conveniently, Squish creates a separate variable for each field's data, something that we can make use of to make the code fully data driven.

The next step is to replace all occurences of hard-coded data with the test data values. Squish also has a convenience function to do this. To use it, select a hard-coded value you want to replace—for example, select the second parameter, "Mr." in selectOption(":_id0:title_select-one", "Mr."). Now right-click to pop up the context menu and this time choose the Replace with addresses.tsv Field. This opens a new submenu from which you can choose the field to use when replacing the selected text. Choose Field 'Title' and Squish will automatically replace the hard-coded value with something like testData.field(address_set[index], "Title"). Repeat this for all other hard-coded values so that the script now reads all of its data from the test data file.

Since we are now testing multiple addresses rather than just one, the original behavior where in the event of a test.verify failing the script closed the browser and cancelled is no longer appropriate. Instead, in the event of a failure we want the form to be reset and the loop continued from the top with the next address. To accomplish this, we need to replace the two return statements with continue statements (which is valid since the relevant bits of the code are now inside a loop).

Although it is convenient to use Squish's menu options to perform the conversion, it can be done just as easily by hand. Either way, at the end the first part of the test case should now look something like this (the second part follows):

Python
def main():
    # open the URL and open the dataset
    loadUrl(":http:#address.icefaces.org/address/")
    for record in testData.dataset("addresses.tsv"):
        # read record fields
        title = testData.field(record, "Title")
        firstName = testData.field(record, "FirstName")
        lastName = testData.field(record, "LastName")
        city = testData.field(record, "City")
        state = testData.field(record, "State")
        zip = testData.field(record, "ZIP")
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one")
        # check that the submit button is disabled
        test.compare(findObject(":_id0:Submit_image").disabled, True)
        # enter some data
        selectOption(":_id0:title_select-one",
            testData.field(record, "Title"))
        setText(":_id0:firstName_text",
            testData.field(record, "FirstName"))
        setText(":_id0:lastName_text", testData.field(record, "LastName"))
        setText(":_id0:city_text", testData.field(record, "City"))
        # check that after entering a city, the state is automatically
        # chosen correctly
        setFocus(":_id0:lastName_text")
        if not test.verify(waitFor(
            "findObject(':_id0:state_text').value == state", 10000)):
            clickButton(":_id0:Reset_image")
            continue
        # input ZIP
        selectOption(":_id0:zipSelect_select-one",
            testData.field(record, "ZIP"))
JavaScript
function main()
{
    // open the URL and open the dataset
    loadUrl(":http://address.icefaces.org/address/");
    var address_set = testData.dataset("addresses.tsv");
    // go through all records
    for (var index in address_set) {
        // read record fields
        var title = testData.field(address_set[index], "Title");
        var firstName = testData.field(address_set[index], "FirstName");
        var lastName = testData.field(address_set[index], "LastName");
        var city = testData.field(address_set[index], "City");
        var state = testData.field(address_set[index], "State");
        var zip = testData.field(address_set[index], "ZIP");
        // wait for the first entry object to be available
        waitForObject(":_id0:title_select-one");
        // check that the submit button is disabled
        test.compare(findObject(":_id0:Submit_image").disabled, true);
        // enter some data
        selectOption(":_id0:title_select-one",
            testData.field(address_set[index], "Title"));
        setText(":_id0:firstName_text",
            testData.field(address_set[index], "FirstName"));
        setText(":_id0:lastName_text",
            testData.field(address_set[index], "LastName"));
        setText(":_id0:city_text",
            testData.field(address_set[index], "City"));
        // check that after entering a city, the state is automatically
        // chosen correctly
        setFocus(":_id0:lastName_text");  
        if (!test.verify(waitFor(
            "findObject(':_id0:state_text').value == state", 10000))) {
            clickButton(":_id0:Reset_image");
            continue;
        }
        // input ZIP
        selectOption(":_id0:zipSelect_select-one",
        testData.field(address_set[index], "ZIP"));
Perl
sub main
{
    # open the URL and open the dataset
    loadUrl(":http:#address.icefaces.org/address/");
    my @address_set = testData::dataset("addresses.tsv");
    foreach (@address_set) {
        # read record fields
        my $record = $_;
        my $title = testData::field($record, "Title");
        my $firstName = testData::field($record, "FirstName");
        my $lastName = testData::field($record, "LastName");
        my $city = testData::field($record, "City");
        my $state = testData::field($record, "State");
        my $zip = testData::field($record, "ZIP");
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one");
        # check that the submit button is disabled
        test::compare(findObject(":_id0:Submit_image")->disabled, 1);
        # enter some data
        selectOption(":_id0:title_select-one",
            testData::field($record, "Title"));
        setText(":_id0:firstName_text",
            testData::field($record, "FirstName"));
        setText(":_id0:lastName_text",
            testData::field($record, "LastName"));
        setText(":_id0:city_text",
            testData::field($record, "City"));
        # check that after entering a city, the state is automatically
        # chosen correctly
        setFocus(":_id0:lastName_text")
        if (!test::verify(waitFor(
            "findObject(':_id0:state_text')->value eq $state", 10000))) {
            clickButton(":_id0:Reset_image");
            continue;
        }
        # input ZIP
        selectOption(":_id0:zipSelect_select-one",
            testData::field($record, "ZIP"));
Ruby
# encoding: UTF-8
require 'squish'
include Squish

def main
    # open the URL and open the dataset
    loadUrl(":http:#address.icefaces.org/address/")
    TestData.dataset("addresses.tsv").each do |record|
        # read record fields
        title = TestData.field(record, "Title")
        firstName = TestData.field(record, "FirstName")
        lastName = TestData.field(record, "LastName")
        city = TestData.field(record, "City")
        state = TestData.field(record, "State")
        zip = TestData.field(record, "ZIP")
        # wait for the first entry object to be available
        waitForObject(":_id0:title_select-one")
        # check that the submit button is disabled
        Test.compare(findObject(":_id0:Submit_image").disabled, true)
        # enter some data
        selectOption(":_id0:title_select-one",
            TestData.field(record, "Title"))
        setText(":_id0:firstName_text",
            TestData.field(record, "FirstName"))
        setText(":_id0:lastName_text", TestData.field(record, "LastName"))
        setText(":_id0:city_text", TestData.field(record, "City"))
        # check that after entering a city, the state is automatically
        # chosen correctly
        setFocus(":_id0:lastName_text")
        widget = waitForObject(":_id0:state_text", 10000)
        if !widget.value == state
            clickButton(":_id0:Reset_image")
            continue
        end
        # input ZIP
        selectOption(":_id0:zipSelect_select-one",
            TestData.field(record, "ZIP"))
Tcl
proc main {} {
    # open the URL and open the dataset
    invoke loadUrl ":http:#address.icefaces.org/address/"
    set data [testData dataset "addresses.tsv"
    set row 0
    for {} {$row < [llength $data]} {incr row} {
        # read record fields
        set record [lindex $data $row]
        set title [testData field $record "Title"]
        set firstName [testData field $record "FirstName"]
        set lastName [testData field $record "LastName"]
        set city [testData field $record "City"]
        set state [testData field $record "State"]
        set zip [testData field $record "ZIP"]
        # wait for the first entry object to be available
        [waitForObject ":_id0:title_select-one"]
        # check that the submit button is disabled
        test compare [property get [findObject \
            ":_id0:Submit_image"] disabled] true
        # enter some data
        invoke selectOption ":_id0:title_select-one" \
            [testData field $record "Title"]
        invoke setText ":_id0:firstName_text" \
            [testData field $record "FirstName"]
        invoke setText ":_id0:lastName_text" \
            [testData field $record "LastName"]
        invoke setText ":_id0:city_text" \
            [testData field $record "City"]
        # check that after entering a city, the state is automatically
        # chosen correctly
        invoke setFocus ":_id0:lastName_text"
        set widget [waitForObject ":_id0:state_text" 10000]
        if {[property get $widget value] != $state} {
            invoke clickButton ":_id0:Reset_image"
            continue
        }
        # input ZIP
        invoke selectOption ":_id0:zipSelect_select-one" \
            [testData field $record "ZIP"]

And the second part of the test script should look like this:

Python
        # check that the submit button is now enabled
        setFocus(":_id0:lastName_text")
        if not test.verify(waitFor(
            "findObject(':_id0:Submit_image').disabled == False", 10000)):
            clickButton(":_id0:Reset_image")
            continue
        # submit
        clickButton(":_id0:Submit_image")
        # wait for the results page
        waitForContextExists(":response.iface")
        waitForObject(":_id1:_id3_SPAN")
        # verify that the data was stored and displayed correctly
        test.compare(findObject(":_id1:_id3_SPAN").innerText,
            testData.field(record, "FirstName"))
        test.compare(findObject(":_id1:_id6_SPAN").innerText,
            testData.field(record, "State"))
        # go back to enter another address
        clickButton(":_id1:Return_image")
    # close browser
    closeWindow(":[Window]")
JavaScript
        // check that the submit button is now enabled
        setFocus(":_id0:lastName_text");
        if (!test.verify(waitFor(
            "findObject(':_id0:Submit_image').disabled == false", 10000))) {
            clickButton(":_id0:Reset_image");
            continue;
        }
        // submit
        clickButton(":_id0:Submit_image");
        // wait for the results page
        waitForContextExists(":response.iface");
        waitForObject(":_id1:_id3_SPAN");
        // verify that the data was stored and displayed correctly
        test.compare(findObject(":_id1:_id3_SPAN").innerText,
            testData.field(address_set[index], "FirstName"));
        test.compare(findObject(":_id1:_id6_SPAN").innerText,
            testData.field(address_set[index], "State"));
        // go back to enter another address
        clickButton(":_id1:Return_image");
    }
    // close browser
    closeWindow(":[Window]");
}
Perl
        # check that the submit button is now enabled
        setFocus(":_id0:lastName_text");
        if (!test::verify(waitFor(
            "findObject(':_id0:Submit_image')->disabled == 0", 10000))) {
            clickButton(":_id0:Reset_image");
            continue;
        }
        # submit
        clickButton(":_id0:Submit_image");
        # wait for the results page
        waitForContextExists(":response.iface");
        waitForObject(":_id1:_id3_SPAN");
        # verify that the data was stored and displayed correctly
        test::compare(findObject(":_id1:_id3_SPAN")->innerText,
            testData::field($record, "FirstName"));
        test::compare(findObject(":_id1:_id6_SPAN")->innerText,
            testData::field($record, "State"));
        # go back to enter another address
        clickButton(":_id1:Return_image");
    }
    # close browser
    closeWindow(":[Window]");
}
Ruby
        # check that the submit button is now enabled
        setFocus(":_id0:lastName_text")
        widget = waitForObject(":_id0:Submit_image", 10000)
        if Test.verify(widget.disabled != False)
            clickButton(":_id0:Reset_image")
            next
        end
        # submit
        clickButton(":_id0:Submit_image")
        # wait for the results page
        waitForContextExists(":response.iface")
        waitForObject(":_id1:_id3_SPAN")
        # verify that the data was stored and displayed correctly
        Test.compare(findObject(":_id1:_id3_SPAN").innerText,
            TestData.field(record, "FirstName"))
        Test.compare(findObject(":_id1:_id6_SPAN").innerText,
            TestData.field(record, "State"))
        # go back to enter another address
        clickButton(":_id1:Return_image")
    end
    # close browser
    closeWindow(":[Window]")
end
Tcl
        # check that the submit button is now enabled
        invoke setFocus ":_id0:lastName_text"
        set widget [waitForObject ":_id0:Submit_image" 10000]
        if {test compare [property get $widget disabled] true} {
            invoke clickButton ":_id0:Reset_image"
            continue
        }
        # submit
        invoke clickButton ":_id0:Submit_image"
        # wait for the results page
        invoke waitForContextExists ":response.iface"
        [waitForObject ":_id1:_id3_SPAN"]
        # verify that the data was stored and displayed correctly
        test compare [property get [findObject \
            ":_id1:_id3_SPAN"] innerText]
            [testData field $record "FirstName"]
        test compare [property get [findObject \
            ":_id1:_id6_SPAN"] innerText]
            [testData field $record "State"]
        # go back to enter another address
        invoke clickButton ":_id1:Return_image"
    }
    # close browser
    invoke closeWindow ":[Window]"
}

The complete JavaScript version of the test script can also be found in squish/examples/web/suite_examples/tst_icefaces_addressbook_datadriven.

17.3.12. How to Do Web Application Load Testing

This example demonstrates how to load test a Web server using Squish. The following set up is assumed: Machine L controls the execution of the load testing scripts and has Squish installed along with a Python interpreter. Machine L also has the test suite to be executed. Machines C1 to Cn are the ones where the the Web browsers will be running. They all need to the squishserver executable installed and running. Machine W is the Web server that will be put under load.

As machine W (the Web server) and machine L controlling the tests are physically different machines we need a way to retrieve system information over the network. The Simple Network Management Protocol (SNMP) is ideal for this task.

The load test is done by a Python script (loadtest.py) which is supplied along with Squish's examples: examples/loadtesting/loadtest.py. The script starts all the squishrunner processes and makes them connect to the squishserver processes and the machines C1 to Cn. All the details about the number of instances, start delays, the target host etc., are defined and documented at the top of the script. Simply adapt them to your needs.

The script will record the start and end times of all squishrunner runs. And in a secondary thread the script polls for system information from the webserver and records this too. Data about both the loads and the runs are written to tab separated values files and can therefore be easily evaluated in succeeding steps. (Or you can modify the script to produce the output in another format if you prefer.)

To make squishrunner be able to connect to remote machines, on each remote machine the configuration option ALLOWED_HOSTS in the squishrunnerrc file must be set to the host starting the squishrunner processes on all those machines C1 to Cn. (see Distributed Tests (Section 19.1.2)).

17.3.12.1. Recording system information

Machine W (the Web server) needs to run a SNMP daemon. It can be installed and set up easily. For configuring, the snmpconf command may prove useful. For example:

snmpconf -g basic_setup

Sources, binaries, documentation, and tutorials for this tool can be found at http://net-snmp.sourceforge.net. In addition the load testing script uses the command line SNMP utilities, which are also needed on Machine L. It would also be possible to use a Python SNMP module from its standard library or a third-party SNMP module—that's something you might want to do for yourself if you find you use the script a lot.

So in parallel with the squishrunners we will record system information using the command line SNMP utilities. At the end of the script the information is written to a file. Be aware that you may need to adjust the usage of the snmpget program to match your SNMP settings. The cutAvg function is a helper function for extracting what we need from the snmpget program's output.

Python
def cutAvg(snmpString):
    return snmpString.strip().rsplit("STRING: ", 1)[1]


loads = []
def sysinfo():
    def loadOne(number):
        cmd = "snmpget -v 1 -c commro %s laLoad.%d" % (
            WEBSERVER_HOST, number)
        tmp = os.popen(cmd, "r")
        reply = tmp.read()
        reply = cutAvg(reply)
        tmp.close()
        return reply

    while True:
        l1 = loadOne(1)
        l5 = loadOne(2)
        l15 = loadOne(3)
        loads.append({'ts': time.time(), '1m': l1, '5m': l5, '15m': l15})
        time.sleep(5)

17.3.12.2. Generating the load

We will store information (host, start time, and end time), related to every squishrunner run in a SquishrunnerRun object. Taking a SquishrunnerRun object as parameter the runsuite function sets the starttime variable, initiates the connection to the squishserver on the given host, and stores the endtime after the squishrunner has finished.

Python
class SquishrunnerRun:
    id = 0
    starttime = 0
    endtime = 0
    host = ''
    
    def __init__(self, id, host):
        self.id = id
        self.host = host
    
    def duration(self):
        return self.endtime - self.starttime


def runSuite(srr):
    srr.starttime = time.time()
    srrCmd = " ".join([SQUISHRUNNER_EXEC, "--host", srr.host,
                       "--testsuite", TEST_SUITE,
                       "--reportgen xml,%s%d.xml" % (REPORTS_DIR, srr.id)])
    os.system(srrCmd)
    srr.endtime = time.time()
    print "call %d finished; needed %s" % (
        srr.id, srr.endtime - srr.starttime)

Having defined the SquishrunnerRun class and the runSuite function we are now able to start the testing itself. For each of the RUN_COUNT runs, the next host will be associated with a newly created SquishrunnerRun object. The next runSuite function call will be started within a new thread. After waiting a specified amount of time we will continue. In addition we store all the SquishrunnerRun objects in a list.

Python
runs = []
for i in range(RUN_COUNT):
    tmp = SquishrunnerRun(i,
        SQUISHSERVER_HOSTS[i % len(SQUISHSERVER_HOSTS)])
    runs.append(tmp)
    thread.start_new_thread(runSuite, (tmp,))
    time.sleep(RUN_DELAY)

Now having started all the squishrunner processes, the script must wait until all of them are finished. If a squishrunner process has finished, its endtime is set. So we must wait until none of the SquishrunnerRun's endtimes are set to 0.

Python
def allRunnersFinished():
    for runner in runs:
        if runner.endtime != 0:
            return False
    return True

while not allRunnersFinished():
    pass

17.3.12.3. Postprocessing

Once the testing itself has finished we must store the results of the test. They will be written to the two files defined as RUNS_FILE and LOADS_FILE.

Python
fh = None
try:
    fh = open(RUNS_FILE, "wt")
    for e in runs:
        fh.write("%s\t%s\t%s\n" % (e.id, e.starttime, e.endtime))
finally:
    if fh is not None:
        fh.close()

fh = None
try:
    fh = open(LOADS_FILE, "wt")
    for e in loads:
        fh.write("%(ts)s\t%(1m)s\t%(5m)s\t%(15m)s\n" % e)
finally:
    if fh is not None:
        fh.close()

The RUNS_FILE contains time stamps marking the start and end of each individual squishrunner run. The LOADS_FILE contains server load averages measured every 5 seconds. The measurement of other information (traffic, number of processes, disk I/O) can easily be configured using suitable SNMP commands. Graphical presentation of the data can be produced with standard charting software.

At some point we hope to provide a ready-made front-end that will make it possible to configure, schedule, and execute test runs, as well provide visual representations of the results.




[20] Squish can also import .tsv (tab-separated values format), .csv (comma-separated values format), and .xls (Microsoft® Excel™ spreadsheet format—but not .xlsx format) files. Both .csv and .tsv files are assumed to use the Unicode UTF-8 encoding—the same encoding used for all test scripts.