How to Test Web Applications
Squish's Web-specific API enables you to find and query objects, access properties and methods, and evaluate arbitrary JavaScript code in the Web-application's context:
- How to Find and Query Web Objects
- How to Use XPath
- How to Access Web Object Properties
- How to Call Web Object Functions
- How to Use evalJS
- How to Use retrieveJSObject
- How to Use the Web Convenience API
- How to Synchronize Web Page Loading for Testing
- How to Test Web Elements
- How to Do Web Application Load Testing
In addition, the Web convenience API provides functions for executing common actions on Web sites, such as clicking a button or entering some text.
For examples of how to use the scripting Web API to access and test complex Web elements, see How to Test Web Elements.
Note: There are two ways to work with web applications: directly, or using the web proxy mechanism. For testing web applications, 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.
How to Find and Query Web Objects
Squish provides two functions—Object findObject(objectName) and Object waitForObject(objectOrName)—that return a reference to the object (HTML or DOM element), for a given qualified object name. The difference between them is that Object waitForObject(objectOrName) 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 Object findObject(objectName) can be used on hidden objects.
See the Web Object API for full details of Squish's Web classes and methods.
There are several ways to identify 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
orinnerText
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 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 Object findObject(objectName) 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 Object findObject(objectName) without risking an error being thrown, by using the Boolean object.exists(objectName) function:
radioName = ("{tagName='INPUT' id='r1' name='rg' form='myform' " + "type='radio' value='Radio 1'}") if object.exists(radioName): radioButton = findObject(radioName) clickButton(radioButton)
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); }
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); }
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
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 Boolean object.exists(objectName) call.
An alternative approach is to use the Object waitForObject(objectOrName) function:
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}") clickButton(radioButton)
var radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}"); clickButton(radioButton);
my $radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " . "form='myform' type='radio' value='Radio 1'}"); clickButton($radioButton);
radioButton = waitForObject("{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}") clickButton(radioButton)
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 Object findObject(objectName) and Object waitForObject(objectOrName) 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.
How to Use XPath
For every object returned by the Object findObject(objectName) and Object waitForObject(objectOrName) 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 id
"mydiv", we can use the following code:
div = findObject("{tagName='DIV' id='mydiv'}") link = div.evaluateXPath("A[contains(@href," + "'www.froglogic.com')]").snapshotItem(0)
var div = findObject("{tagName='DIV' id='mydiv'}"); var link = div.evaluateXPath("A[contains(@href," + "'www.froglogic.com')]").snapshotItem(0);
my $div = findObject("{tagName='DIV' id='mydiv'}"); my $link = $div->evaluateXPath("A[contains(@href," . "'www.froglogic.com')]")->snapshotItem(0);
div = findObject("{tagName='DIV' id='mydiv'}") link = div.evaluateXPath("A[contains(@href," + "'www.froglogic.com')]").snapshotItem(0)
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.
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_XPathResult HTML_Object.evaluateXPath(statement) method returns an object of type HTML_XPathResult Class on which you can query the result of the XPath evaluation.
How to Access Table Cell Contents has an example of using the HTML_XPathResult HTML_Object.evaluateXPath(statement) method to extract the contents of an HTML table's cell.
For more information about how you can create XPath queries to help produce flexible and compact test scripts, see XPath Tutorial from the W3Schools Online Web Tutorials website.
See also the Squish for Web tutorial Inserting Additional Verification Points.
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 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.
entry = waitForObject( "{tagName='INPUT' id='input' form='myform' type='text'}") entry.value = "Some new text" test.log(entry.value)
var entry = waitForObject( "{tagName='INPUT' id='input' form='myform' type='text'}"); entry.value = "Some new text"; test.log(entry.value);
my $entry = waitForObject( "{tagName='INPUT' id='input' form='myform' type='text'}"); $entry->value = "Some new text"; test::log($entry->value);
entry = waitForObject( "{tagName='INPUT' id='input' form='myform' type='text'}") entry.value = "Some new text" Test.log(entry.value)
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:
div = findObject("DOCUMENT.HTML1.BODY1......DIV") test.compare(div.property("offsetWidth"), 18)
var div = findObject("DOCUMENT.HTML1.BODY1......DIV"); test.compare(div.property("offsetWidth"), 18);
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV"); test::compare($div->property("offsetWidth"), 18);
div = findObject("DOCUMENT.HTML1.BODY1......DIV") Test.compare(div.property("offsetWidth"), 18)
set div [findObject "DOCUMENT.HTML1.BODY1......DIV"] test compare [invoke $div property "offsetWidth"] 18
Note that for hidden elements we must always use the Object findObject(objectName) function rather than the Object waitForObject(objectOrName) function.
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.
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 HTML_Object.firstChild() function:
div = findObject("DOCUMENT.HTML1.BODY1......DIV") child = div.firstChild() test.log(child.tagName)
var div = findObject("DOCUMENT.HTML1.BODY1......DIV"); var child = div.firstChild(); test.log(child.tagName);
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV"); my $child = $div->firstChild(); test::log($child->tagName);
div = findObject("DOCUMENT.HTML1.BODY1......DIV") child = div.firstChild() Test.log(child.tagName)
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:
element = findObject( ":{tagName='INPUT' id='sel' form='myform' type='select-one'}") option = element.optionAt(element.selectedIndex) test.log(option.text)
var element = findObject( ":{tagName='INPUT' id='sel' form='myform' type='select-one'}"); var option = element.optionAt(element.selectedIndex); test.log(option.text);
my $element = findObject( ":{tagName='INPUT' id='sel' form='myform' type='select-one'}"); my $option = $element->optionAt($element->selectedIndex); test::log($option->text);
element = findObject( ":{tagName='INPUT' id='sel' form='myform' type='select-one'}") option = element.optionAt(element.selectedIndex) Test.log(option.text)
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:
div = findObject("DOCUMENT.HTML1.BODY1......DIV") div.invoke("customFunction", "an argument")
var div = findObject("DOCUMENT.HTML1.BODY1......DIV"); div.invoke("customFunction", "an argument");
my $div = findObject("DOCUMENT.HTML1.BODY1......DIV"); $div->invoke("customFunction", "an argument");
div = findObject("DOCUMENT.HTML1.BODY1......DIV") div.invoke("customFunction", "an argument")
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: ...
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 Object evalJS(browserTab, code) function. Here is an example of its use:
style_display = evalJS("var d = document.getElementById(" + "'busyDIV'); d ? d.style.display : ''")
var style_display = evalJS("var d = document.getElementById(" + "'busyDIV'); d ? d.style.display : ''");
my $style_display = evalJS("var d = document.getElementById(" . "'busyDIV'); d ? d.style.display : ''");
style_display = evalJS("var d = document.getElementById(" + "'busyDIV'); d ? d.style.display : ''")
set style_display [invoke evalJS "var d = document.getElementById(\ 'busyDIV'); d ? d.style.display : ''"]
The Object evalJS(browserTab, code) 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.
How to Use retrieveJSObject
In addition to test scripts being able to run some JavaScript snippet and retrieve the string value of the result, Squish also can 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 instead returns an object itself. In such a case, the reference allows retrieving properties from that object or calling methods on that object. For this purpose, Squish provides the JsObject retrieveJSObject(javascriptcode) function. Here is an example of its use:
jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;")
var jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;");
my $jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;");
jsobject = retrieveJSObject("var globalObject = { 'id': 'obj1', 'name': function() { return 'name1'; } };globalObject;")
set jsobject [invoke retrieveJSObject {var globalObject = \{ 'id': 'obj1', 'name': function() \{ return 'name1'; \} \};globalObject;\} }]
The JsObject retrieveJSObject(javascriptcode) 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
test.log("id: " + jsobject.property("id")) test.log("name: " + jsobject.call("name"))
test.log("id: " + jsobject.property("id")); test.log("name: " + jsobject.call("name"));
test::log("id: " . $jsobject->property("id")); test::log("name: " . $jsobject->call("name"));
test.log("id: " + jsobject.property("id")) test.log("name: " + jsobject.call("name"))
test log "id: " [invoke $jsobject property "id"] test log "name: " [invoke $jsobject call "name"]
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 in the Tools Reference. 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.
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")
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");
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");
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")
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(objectOrName), selectOption(objectOrName, text), and setText(objectOrName, text)), although all of them are just as easy to use.
How to Synchronize Web Page Loading for Testing
In many simple cases, just waiting for a particular object to become available using the Object waitForObject(objectOrName) 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 Boolean isPageLoaded(browserTab) 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:
loaded = waitFor("isPageLoaded()", 5000) if loaded: clickButton(waitForObject( ":{tagName='INPUT' type='button' value='Login'}")) else: test.fatal("Page loading failed")
var loaded = waitFor("isPageLoaded()", 5000); if (loaded) clickButton(waitForObject( ":{tagName='INPUT' type='button' value='Login'}")); else test.fatal("Page loading failed");
my $loaded = waitFor("isPageLoaded()", 5000); if ($loaded) { clickButton(waitForObject( ":{tagName='INPUT' type='button' value='Login'}")); } else { test::fatal("Page loading failed"); }
loaded = waitFor("isPageLoaded", 5000) if loaded clickButton(waitForObject( ":{tagName='INPUT' type='button' value='Login'}")) else Test.fatal("Page loading failed") end
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 Boolean isPageLoaded(browserTab) 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 Object waitForObject(objectOrName) function—and we may even have to specify a longer timeout than the default 20 000 milliseconds to allow for network latency.
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 in Tutorial: Starting to Test Web Applications, 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 flexible—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 Object waitForObject(objectOrName) 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) and adding the object to the Object Map (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, which we can then copy and paste into our code.
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.
entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}") test.verify(not entry.disabled)
var entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}"); test.verify(!entry.disabled);
my $entry = waitForObject("{tagName='INPUT' id='input' " . "form='myform' type='text'}"); test::verify(!$entry->disabled);
entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}") Test.verify(!entry.disabled)
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).
Form Checkboxes and Radiobuttons
To verify that a radiobutton or checkbox is checked, we just need to query its checked
property.
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}") test.verify(radiobutton.checked)
var radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}"); test.verify(radiobutton.checked);
my $radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " . "form='myform' type='radio' value='Radio 1'}"); test::verify($radiobutton->checked);
radiobutton = waitForObject(":{tagName='INPUT' id='r1' name='rg' " + "form='myform' type='radio' value='Radio 1'}") Test.verify(radiobutton.checked)
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.
Form Text fields
Both the text
and textarea
form elements have a value
property, so it is easy to check what they contain.
entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}") test.compare(entry.value, "Ternary")
var entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}"); test.compare(entry.value, "Ternary");
my $entry = waitForObject("{tagName='INPUT' id='input' " . "form='myform' type='text'}"); test::compare($entry->value, "Ternary");
entry = waitForObject("{tagName='INPUT' id='input' " + "form='myform' type='text'}") Test.compare(entry.value, "Ternary")
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.
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.
selection = waitForObject(":{tagName='INPUT' id='sel' " + "form='myform' type='select-one'}") test.compare(selection.selectedIndex, 2) test.compare(selection.selectedOption, "Cavalier")
var selection = waitForObject(":{tagName='INPUT' id='sel' " + "form='myform' type='select-one'}"); test.compare(selection.selectedIndex, 2); test.compare(selection.selectedOption, "Cavalier");
my $selection = waitForObject(":{tagName='INPUT' id='sel' " . "form='myform' type='select-one'}"); test::compare($selection->selectedIndex, 2); test::compare($selection->selectedOption, "Cavalier");
selection = waitForObject(":{tagName='INPUT' id='sel' " + "form='myform' type='select-one'}") Test.compare(selection.selectedIndex, 2) Test.compare(selection.selectedOption, "Cavalier")
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".
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")
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");
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");
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")
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 multiple 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 class, its HTML_Option HTML_Select.optionAt(index) function, and its text
and selected
properties.
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 Object findObject(objectName) function and the Object waitForObject(objectOrName) function have an HTML_XPathResult HTML_Object.evaluateXPath(statement) 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:
def getCellText(tableObject, row, column): return tableObject.evaluateXPath("TBODY/TR[%d]/TD[%d]" % ( row + 1, column + 1)).stringValue
function getCellText(tableObject, row, column) { return tableObject.evaluateXPath("TBODY/TR[" + (row + 1) + "]/TD[" + (column + 1) + "]").stringValue; }
sub getCellText { my ($tableObject, $row, $column) = @_; ++$row; ++$column; return $tableObject->evaluateXPath( "TBODY/TR[$row]/TD[$column]")->stringValue; }
def getCellText(tableObject, row, column) tableObject.evaluateXPath( "TBODY/TR[#{row + 1}]/TD[#{column + 1}]").stringValue end
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; 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:
table = waitForObject(htmlTableName) text = getCellText(table, 23, 11)
var table = waitForObject(htmlTableName); var text = getCellText(table, 23, 11);
my $table = waitForObject($htmlTableName); my $text = getCellText($table, 23, 11);
table = waitForObject(htmlTableName) text = getCellText(table, 23, 11)
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.
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".
table = waitForObject("{tagName='TABLE' id='result_table]'}") contents = table.innerText test.verify(contents.find("Total: 387.92") != -1)
var table = waitForObject("{tagName='TABLE' id='result_table]'}"); var contents = table.innerText; test.verify(contents.indexOf("Total: 387.92") != -1);
my $table = waitForObject("{tagName='TABLE' id='result_table]'}"); my $contents = $table->innerText; test::verify(index($contents, "Total: 387.92") != -1);
table = waitForObject("{tagName='TABLE' id='result_table]'}") contents = table.innerText Test.verify(contents.find("Total: 387.92") != -1)
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.
div = waitForObject(":{tagName='DIV' id='syncDIV'}") test.compare(div.style().value("display"), "hidden")
var div = waitForObject(":{tagName='DIV' id='syncDIV'}"); test.compare(div.style().value("display"), "hidden");
my $div = waitForObject(":{tagName='DIV' id='syncDIV'}"); test::compare($div->style()->value("display"), "hidden");
div = waitForObject(":{tagName='DIV' id='syncDIV'}") Test.compare(div.style().value("display"), "hidden")
set div [waitForObject ":{tagName='DIV' id='syncDIV'}"] test compare [invoke $div style [invoke value "display"]] "hidden"
Notice that we must use the HTML_Style 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.
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')")
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')");
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')");
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')")
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 Boolean waitFor(condition) 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.)
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 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).
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.
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)
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.
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.
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 endtime
s are set to 0.
def allRunnersFinished(): for runner in runs: if runner.endtime != 0: return False return True while not allRunnersFinished(): pass
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
.
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.
© 2024 The Qt Company Ltd.
Documentation contributions included herein are the copyrights of
their respective owners.
The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation.
Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property
of their respective owners.