Now that we have seen how to record a test and modify it by inserting verification points, we are ready to see how to create tests manually. The easiest way to do this is to modify and refactor recorded tests, although it is also perfectly possible to create manual tests from scratch.
Potentially the most challenging part of writing manual tests is to use the right object names, but in practice, this is rarely a problem. We can either copy the symbolic names that Squish has already added to the Object Map when recording previous tests, or we can copy object names directly from recorded tests. And if we haven't recorded any tests and are starting from scratch we can use the Spy. We do this by clicking the toolbar button. This starts the AUT and switches to the Squish Spy Perspective (Section 16.1.2.1). We can then interact with the AUT until the object we are interested in is visible. Then, inside the Squish IDE we can navigate to the object in the Application Objects view—or use the toolbar button—and use the context menu to both add the object to the Object Map (so that Squish will remember it) and to the clipboard (so that we can paste it into our test script). And at the end we can click the toolbar button to terminate the AUT and return Squish to the Squish Test Management Perspective (Section 16.1.2.2). (See How to Use the Spy (Section 13.19.3) in the User Guide (Chapter 13) for more details on using the Spy.)
We can view the Object Map by clicking the toolbar button (see also, the Object Map view (Section 16.2.9)). Every application object that Squish interacts with is listed here, either as a top-level object, or as a child object (the view is a tree view). We can retrieve the symbolic name used by Squish in recorded scripts by right-clicking the object we are interested in and then clicking the context menu's Copy item. This is useful for when we want to modify existing test scripts or when we want to create test scripts from scratch, as we will see later on in the tutorial.

Suppose we want to test the AUT's Add functionality by adding three new names and addresses. We could of course record such a test but it is just as easy to do everything in code. The steps we need the test script to do are: first click the New button (and then OK) to create a new address book, then for each new name and address, click the Add button, then fill in the details, and click Save. We also want to verify after clicking New that there are no rows of data and at the end that there are three rows. We will also refactor as we go, to make our code as neat and modular as possible.
First we must create a new empty test case. Click
| and set the test case's name to be
tst_adding. Squish will automatically create an
empty test.js (or test.py, and
so on) file.
Command line users can simply create a tst_adding
directory inside the test suite's directory and create and edit the
test.js file (or test.py and
so on) within that directory.
The first thing we need is a way to start the AUT. Here are the first
few lines from the recorded
tst_general script:
def main():
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
function main() {
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
sub main
{
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
def main
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
proc main {} {
invoke loadUrl "http://localhost:9090/addressbook-4.1.html"
invoke waitForContextExists ":addressbook-4.1.html"
Notice that the pattern in the code is simple: start the AUT, then wait for the page to be ready.
![]() | Note |
|---|---|
It may seem a waste to put our functions in
|
If you look at the recorded test (tst_general) or
in the Object Map you will see that Squish uses hierarchical names
that reflect the DOM (Document Object Model) of the HTML page.
Once we start writing tests, sometimes the AUT will appear to freeze when we run one of our tests. When this happens, just wait for Squish to time out the AUT (about 20 seconds), and then look at the error message in the test log window. If you get an error similar to this:
Error Script Error Apr 9, 2011 Detail LookupError: Item 'New...' in object ':Address Book.File' not found or ready. Called from: C:\squish\examples\web\addressbook\suite_js\tst_adding\test.js: 18 Location C:\squish\examples\web\addressbook\suite_js\tst_adding\test.js:3
don't worry! It just means that Squish doesn't have an object with the given name in the Object Map. We can easily add the names we need either by recording a dummy test and interacting with all the AUT objects we plan to use in our tests or by using the Spy tool. In addition to the Spy's object picker we can also use the Spy's Application Objects view (Section 16.2.1) to locate the objects we are interested in and use the context menu to add them to the Object Map. However, recording a dummy test is often quicker for adding lots of objects to the Object Map, providing we interact with all the AUT objects we are interested in.
We've spent a bit of time on the issue of naming since it is probably the part of writing scripts that leads to the most error messages (usually of the "object ... not found" kind shown above.) Once we have identified the objects we are going to access in our tests, writing test scripts using Squish is very straightforward. And of course you can almost certainly use the scripting language you are most familiar with since Squish supports the most popular ones available.
We are now almost ready to write our own test script. It is probably
easiest to begin by recording a dummy test. So click
| and set the test case's name to be
tst_dummy. Then click the dummy test case's
toolbar button
(
).
Once the AUT starts, click the New button then click OK. This will clear
out the example data and leave the table empty and ready for new data.
Click the Control Bar Window (Section 16.1.3)'s button. Replay this test just to confirm that
everything works okay. The sole purpose of this is to make sure that
Squish adds the necessary names to the Object Map since it is probably
quicker to do it this way than to use the Spy for every object of
interest. After replaying the dummy test you can delete it if you want
to.
With all the object names we need in the Objec Map we can now write our
own test script completely from scratch. We will start with the
main function, and then we will look at the supporting
functions that the main function uses.
def main():
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
confirmPopup(":newButton_button")
test.verify(numberOfRows() == 0, "%d" % numberOfRows())
data = [("Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"),
("Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"),
("Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654")]
for oneNameAndAddress in data:
addNameAndAddress(oneNameAndAddress)
test.verify(numberOfRows() == 3, "%d" % numberOfRows())
function main() {
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
confirmPopup(":newButton_button");
test.verify(numberOfRows() == 0);
var data = new Array(
new Array("Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"),
new Array("Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"),
new Array("Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654"));
for (var row = 0; row < data.length; ++row)
addNameAndAddress(data[row]);
test.verify(numberOfRows() == 3);
}
sub main
{
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
confirmPopup(":newButton_button");
test::verify(numberOfRows() == 0);
my @data = (["Andy", "Beach", "andy.beach\@nowhere.com", "555 123 6786"],
["Candy", "Deane", "candy.deane\@nowhere.com", "555 234 8765"],
["Ed", "Fernleaf", "ed.fernleaf\@nowhere.com", "555 876 4654"]);
foreach $oneNameAndAddress (@data) {
addNameAndAddress(@{$oneNameAndAddress});
}
test::verify(numberOfRows() == 3);
}
def main
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
confirmPopup(":newButton_button")
Test.verify(numberOfRows == 0)
data = [["Andy", "Beach", "andy.beach@nowhere.com", "555 123 6786"],
["Candy", "Deane", "candy.deane@nowhere.com", "555 234 8765"],
["Ed", "Fernleaf", "ed.fernleaf@nowhere.com", "555 876 4654"]]
data.each do |oneNameAndAddress|
addNameAndAddress(oneNameAndAddress)
end
Test.verify(numberOfRows == 3)
end
proc main {} {
invoke loadUrl "http://localhost:9090/addressbook-4.1.html"
invoke waitForContextExists ":addressbook-4.1.html"
confirmPopup ":newButton_button"
test compare [numberOfRows] 0
set data [list \
[list "Andy" "Beach" "andy.beach@nowhere.com" "555 123 6786"] \
[list "Candy" "Deane" "candy.deane@nowhere.com" "555 234 8765"] \
[list "Ed" "Fernleaf" "ed.fernleaf@nowhere.com" "555 876 4654"] ]
for {set i 0} {$i < [llength $data]} {incr i} {
addNameAndAddress [lindex $data $i]
}
test compare [numberOfRows] 3
}
We begin by starting the AUT with a call to the loadUrl function parameterized by the name of
the web page we want it to start with. This means that Squish won't
ask us to confirm the page to load. We then wait for the page to be
available with a call to the waitForContextExists function. Next we call a
custom confirmPopup function that clicks the New button
(which in turn causes an OK/Cancel dialog to appear), and confirms
(i.e., clicks OK). This will empty the table. (We copied the name of the
New button from the Object Map—it was put there by the dummy test
we recorded.) Next we call the custom numberOfRows function
that we created earlier to verify that the table is empty.
Next, we create some sample data and call a custom
addNameAndAddress function to populate the table with the
data using the AUT's Add dialog. And finally, we again compare the table's row
count, this time to the number of rows in our sample data.
We will now review two of the three supporting functions, so as to
cover all the code in the tst_adding test case,
starting with the confirmPopup function. (The third
function, numberOfRows was discussed earlier.)
def confirmPopup(button):
clickButton(waitForObject(button))
snooze(1.8)
closeConfirm(":ConfirmPopup", True)
function confirmPopup(button) {
clickButton(waitForObject(button));
snooze(1.8);
closeConfirm(":ConfirmPopup", true);
}
sub confirmPopup
{
my ($button) = @_;
clickButton(waitForObject($button));
snooze(1.8);
closeConfirm(":ConfirmPopup", 1);
}
def confirmPopup(button)
clickButton(waitForObject(button))
snooze(1.8)
closeConfirm(":ConfirmPopup", true)
end
proc confirmPopup {button} {
invoke clickButton [waitForObject $button]
snooze 1.8
invoke closeConfirm ":ConfirmPopup" true
}
This function is called with the symbolic name of the button we want to
click and confirm. (The name was copied from the Object Map.) After
clicking we force Squish to wait a short time (1.8 seconds using the
snooze function) and then
close the dialog that popped up using the closeConfirm function. The first argument can
be any text but the second must either be true (which means
click OK) or false (which means click Cancel). So here we
have clicked OK.
def addNameAndAddress(oneNameAndAddress):
clickButton(waitForObject(":addButton_button"))
for fieldName, text in zip(("forename", "surname", "email", "phone"),
oneNameAndAddress):
setText(waitForObject(":oneitem.%sEdit_text" % fieldName), text);
clickButton(waitForObject(":saveAddButton_button"))
function addNameAndAddress(oneNameAndAddress) {
clickButton(waitForObject(":addButton_button"));
var fieldNames = new Array("forename", "surname", "email", "phone");
for (var i = 0; i < oneNameAndAddress.length; ++i) {
var fieldName = fieldNames[i];
var text = oneNameAndAddress[i];
setText(waitForObject(":oneitem." + fieldName + "Edit_text"), text);
}
clickButton(waitForObject(":saveAddButton_button"));
}
sub addNameAndAddress
{
my (@oneNameAndAddress) = @_;
clickButton(waitForObject(":addButton_button"));
my @fieldNames = ("forename", "surname", "email", "phone");
my $fieldName = "";
for (my $i = 0; $i < scalar(@fieldNames); $i++) {
$fieldName = $fieldNames[$i];
my $text = $oneNameAndAddress[$i];
setText(waitForObject(":oneitem.${fieldName}Edit_text"), $text);
}
clickButton(waitForObject(":saveAddButton_button"));
}
def addNameAndAddress(oneNameAndAddress)
clickButton(waitForObject(":addButton_button"))
["forename", "surname", "email", "phone"].each_with_index do
|fieldName, index|
text = oneNameAndAddress[index]
setText(waitForObject(":oneitem.#{fieldName}Edit_text"), text)
end
clickButton(waitForObject(":saveAddButton_button"))
end
proc addNameAndAddress {oneNameAndAddress} {
invoke clickButton [waitForObject ":addButton_button"]
set fieldNames [list "forename" "surname" "email" "phone"]
for {set field 0} {$field < [llength $fieldNames]} {incr field} {
set fieldName [lindex $fieldNames $field]
set text [lindex $oneNameAndAddress $field]
invoke setText [waitForObject ":oneitem.${fieldName}Edit_text"] $text
}
invoke clickButton [waitForObject ":saveAddButton_button"]
}
For each set of name and address data we click the Add button to make
the Add form visible. Then for each value received we populate the
appropriate field by waiting for the relevant text field to be
ready and then typing in the text using the setText function.
And at the end we click the form's Save button. We got the line at the
heart of the function by copying it from the recorded
tst_general test and simply parameterizing it by
the field name and text. Similarly, we copied the code for clicking the
Save button from the tst_general test case's code.
The entire test is around 35 lines of code—and would be even less
if we put some of the common functions (such as
confirmPopup and numberOfRows) in
a shared script. And much of the code was copied directly from the
recorded test, and in some cases parameterized.
This should be sufficient to give a flavor of writing test scripts for an AUT. Keep in mind that Squish provides far more functionality than we used here, (all of which is covered in the API Reference Manual (Chapter 14) and the Tools Reference Manual (Chapter 15)). And Squish also provides access to the entire public APIs of the AUT's objects.
However, one aspect of the test case is not very satisfactory. Although embedding test data as we did here is sensible for small amounts, it is rather limiting, especially when we want to use a lot of test data. Also, we didn't test any of the data that was added to see if it correctly ended up in the table. In the next section we will create a new version of this test, only this time we will pull in the data from an external data source, and check that the data we add to the table is correct.
In the previous section we put three hard-coded names and addresses in
our test. But what if we want to test lots of data?
Or what if we want to change the data without having to change our test
script's source code. One approach is to import a dataset into Squish
and use the dataset as the source of the values we insert into our
tests. Squish can import data in .tsv
(tab-separated values format), .csv
(comma-separated values format), and .xls
(Microsoft® Excel™ spreadsheet format—but
not .xlsx format).
[7]
Test data can either be imported using the Squish IDE, or manually using a file manager or console commands. We will describe both approaches, starting with using the Squish IDE.
For the addressbook application we want to
import the MyAddresses.tsv data file. To do this we
must start by clicking
| to pop-up the Import Squish Resource dialog (Section 16.3.3). Inside the dialog click
the button to choose the file to
import—in this case MyAddresses.tsv. Make
sure that the Import As combobox is set to
“TestData”. By default the Squish IDE will import the test data
just for the current test case, but we want the test data to be
available to all the test suite's test cases: to do this check the
radio button. Now
click the button. You can now see the file
listed in the Test Suite Resources view (in the Test Data tab), and if
you click the file's name it will be shown in an Editor view (Section 16.2.6). The screenshot shows Squish after the
test data has been added.
![]() | For command-line users |
|---|---|
It is also possible to import test data outside the Squish IDE using a file
manager (such as File Explorer) or console commands. To do this, create
a directory inside the test suite's directory called
|

Although in real life we would modify our
tst_adding test case to use the test data, for the
purpose of the tutorial we will make a new test case called
tst_adding_data that is a copy of
tst_adding and which we will modify to make use of
the test data.
The only function we have to change is main, where
instead of iterating over hard-coded items of data, we iterate over all
the records in the dataset. We also need to update the expected row
count at the end since we are adding a lot more records now, and we will
also add a function to verify each record that's added.
def main():
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
confirmPopup(":newButton_button")
test.verify(numberOfRows() == 0)
limit = 10
for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
forename = testData.field(record, "Forename")
surname = testData.field(record, "Surname")
email = testData.field(record, "Email")
phone = testData.field(record, "Phone")
addNameAndAddress((forename, surname, email, phone)) # pass as a single tuple
checkNameAndAddress(record)
if row > limit:
break
test.verify(numberOfRows() == row + 1)
function main() {
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
confirmPopup(":newButton_button");
test.verify(numberOfRows() == 0);
var limit = 10;
var records = testData.dataset("MyAddresses.tsv");
for (var row = 0; row < records.length; ++row) {
var record = records[row];
var forename = testData.field(record, "Forename");
var surname = testData.field(record, "Surname");
var email = testData.field(record, "Email");
var phone = testData.field(record, "Phone");
addNameAndAddress(new Array(forename, surname, email, phone));
checkNameAndAddress(record);
if (row > limit)
break;
}
test.verify(numberOfRows() == row + 1);
}
sub main
{
loadUrl("http://localhost:9090/addressbook-4.1.html");
waitForContextExists(":addressbook-4.1.html");
confirmPopup(":newButton_button");
test::verify(numberOfRows() == 0);
my @records = testData::dataset("MyAddresses.tsv");
my $limit = 10;
my $row = 0;
for (; $row < scalar(@records); ++$row) {
my $record = $records[$row];
my $forename = testData::field($record, "Forename");
my $surname = testData::field($record, "Surname");
my $email = testData::field($record, "Email");
my $phone = testData::field($record, "Phone");
addNameAndAddress(($forename, $surname, $email, $phone));
checkNameAndAddress($record);
if ($row > $limit) {
last;
}
}
test::verify(numberOfRows() == $row + 1);
}
def main
loadUrl("http://localhost:9090/addressbook-4.1.html")
waitForContextExists(":addressbook-4.1.html")
confirmPopup(":newButton_button")
Test.verify(numberOfRows == 0)
limit = 10
rows = 0
TestData.dataset("MyAddresses.tsv").each_with_index do
|record, row|
forename = TestData.field(record, "Forename")
surname = TestData.field(record, "Surname")
email = TestData.field(record, "Email")
phone = TestData.field(record, "Phone")
addNameAndAddress([forename, surname, email, phone]) # pass as an Array
checkNameAndAddress(record)
break if row > limit
rows += 1
end
Test.verify(numberOfRows == rows + 1)
end
proc main {} {
invoke loadUrl "http://localhost:9090/addressbook-4.1.html"
invoke waitForContextExists ":addressbook-4.1.html"
confirmPopup ":newButton_button"
test compare [numberOfRows] 0
set limit 10
set data [testData dataset "MyAddresses.tsv"]
set columns [llength [testData fieldNames [lindex $data 0]]]
set row 0
for {} {$row < [llength $data]} {incr row} {
set record [lindex $data $row]
set forename [testData field $record "Forename"]
set surname [testData field $record "Surname"]
set email [testData field $record "Email"]
set phone [testData field $record "Phone"]
set details [list $forename $surname $email $phone]
addNameAndAddress $details
checkNameAndAddress $record
if {$row > $limit} {
break
}
}
test compare [numberOfRows] [expr $row + 1]
}
Squish provides access to test data through its testData
module's functions—here we used the testData.dataset function to access the data file
and make its records available, and the testData.field function to retrieve each record's
individual fields.
Having used the test data to populate the HTML table we
want to be confident that the data in the table is the same as what we
have added, so that's why we added the
checkNameAndAddress function. We also added a limit to
how many records we would compare, just to make the test run faster.
def checkNameAndAddress(record):
table = waitForObject(":DOCUMENT.HTML1.BODY1.DIV1.DIV1.FORM1.TABLE1")
cells = (table.evaluateXPath(".//TR/TD[@class='forename']"),
table.evaluateXPath(".//TR/TD[@class='surname']"),
table.evaluateXPath(".//TR/TD[@class='email']"),
table.evaluateXPath(".//TR/TD[@class='phone']"))
for column in range(len(testData.fieldNames(record))):
cell = cells[column].singleNodeValue().innerText
field = testData.field(record, column)
test.compare(cell, field)
function checkNameAndAddress(record)
{
var table = waitForObject(":DOCUMENT.HTML1.BODY1.DIV1.DIV1.FORM1.TABLE1");
var cells = [table.evaluateXPath(".//TR/TD[@class='forename']"),
table.evaluateXPath(".//TR/TD[@class='surname']"),
table.evaluateXPath(".//TR/TD[@class='email']"),
table.evaluateXPath(".//TR/TD[@class='phone']")];
for (var column = 0; column < testData.fieldNames(record).length;
++column) {
var cell = cells[column].singleNodeValue().innerText;
var field = testData.field(record, column);
test.compare(cell, field);
}
}
sub checkNameAndAddress
{
my ($record) = @_;
my $table = waitForObject(":DOCUMENT.HTML1.BODY1.DIV1.DIV1.FORM1.TABLE1");
my @cells = ($table->evaluateXPath(".//TR/TD[\@class='forename']"),
$table->evaluateXPath(".//TR/TD[\@class='surname']"),
$table->evaluateXPath(".//TR/TD[\@class='email']"),
$table->evaluateXPath(".//TR/TD[\@class='phone']"));
my @columnNames = testData::fieldNames($record);
for (my $column = 0; $column < scalar(@columnNames); ++$column) {
my $cell = $cells[$column]->singleNodeValue()->innerText;
my $field = testData::field($record, $column);
test::compare($cell, $field);
}
}
def checkNameAndAddress(record)
table = waitForObject(":DOCUMENT.HTML1.BODY1.DIV1.DIV1.FORM1.TABLE1")
cells = [table.evaluateXPath(".//TR/TD[@class='forename']"),
table.evaluateXPath(".//TR/TD[@class='surname']"),
table.evaluateXPath(".//TR/TD[@class='email']"),
table.evaluateXPath(".//TR/TD[@class='phone']")]
for column in 0...TestData.fieldNames(record).length
cell = cells[column].singleNodeValue().innerText
field = TestData.field(record, column)
Test.compare(cell, field)
end
end
proc checkNameAndAddress {record} {
set table [waitForObject ":DOCUMENT.HTML1.BODY1.DIV1.DIV1.FORM1.TABLE1"]
set cells [list \
[invoke $table evaluateXPath {.//TR/TD[@class='forename']}] \
[invoke $table evaluateXPath {.//TR/TD[@class='surname']}] \
[invoke $table evaluateXPath {.//TR/TD[@class='email']}] \
[invoke $table evaluateXPath {.//TR/TD[@class='phone']}]]
set columns [llength [testData fieldNames $record]]
for {set column 0} {$column < $columns} {incr column} {
set cell [property get [invoke [lindex $cells $column] \
singleNodeValue] innerText]
set field [testData field $record $column]
test compare $cell $field
}
}
The calls to the HTML_Object.evaluateXPath function use XPath
queries to access the HTML table and find the first cells that match.
Since the addressbook application always adds new addresses at the start
the query works perfectly. We use Squish's testData.fieldNames function to get a column count
and then use the test.compare function to
check that each value in the table is the same as the value in the test
data we used.
The screenshot show Squish's Test Summary log after the data-driven tests have been run.

Squish can also do keyword-driven testing. This is a bit more sophisticated than data-driven testing. See How to Do Keyword-Driven Testing (Section 13.14).
[7]
Both .csv and .tsv files are
assumed to use the Unicode UTF-8 encoding—the same encoding used
for all test scripts.