12.4. Creating Tests by Hand

12.4.1. Modifying and Refactoring Recorded Tests
12.4.2. Creating Data Driven Tests

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 Launch AUT toolbar button. This starts the AUT and switches to the Squish Spy Perspective (Section 17.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 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 Quit AUT toolbar button to terminate the AUT and return Squish to the Squish Test Management Perspective (Section 17.1.2.2). (See How to Use the Spy (Section 15.2.3) in the User Guide (Chapter 15) for more details on using the Spy.)

We can view the Object Map by clicking the Object Map toolbar button (see also, the Object Map view (Section 17.2.8)). 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.

Squish's Object Map

12.4.1. Modifying and Refactoring Recorded Tests

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 File|New to create a new address book, then for each new name and address, click Edit|Add, then fill in the details, and click OK. And finally, click File|Quit without saving. We also want to verify at the start 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 File|New Test Case... and set the test case's name to be tst_adding. Squish will automatically create an empty test.tcl (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.tcl file (or test.py and so on) within that directory.

The first thing we need is a way to start the AUT and then invoke a menu option. Here are the first few lines from the recorded tst_general script:

Tcl
proc main {} {
    startApplication "addressbook.tcl"
    waitForObjectItem ":addressbook\\.tcl.#menuBar" "File"
    invoke activateItem ":addressbook\\.tcl.#menuBar" "File"
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#file" "Open..."
    invoke activateItem ":addressbook\\.tcl.#menuBar.#file" "Open..."

Python
def main():
    startApplication("addressbook.tcl")
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File")
    activateItem(":addressbook\\.tcl.#menuBar", "File")
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...")
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...")

JavaScript
function main()
{
    startApplication("addressbook.tcl");
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File");
    activateItem(":addressbook\\.tcl.#menuBar", "File");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...");

Perl
sub main
{
    startApplication("addressbook.tcl");
    waitForObjectItem(":addressbook\\.tcl.#menuBar", "File");
    activateItem(":addressbook\\.tcl.#menuBar", "File");
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#file", "Open...");
    activateItem(":addressbook\\.tcl.#menuBar.#file", "Open...");

Notice that the pattern in the code is simple: start the AUT, then wait for the menu bar, then activate the menu bar; wait for the menu item, then activate the menu item. In both cases we have used the waitForObjectItem function. This function is used for a multi-valued objects (such as lists, tables, trees—or in this case, a menubar and a menu), and allows us to access the object's items (which are themselves objects of course), by passing the name of the object containing the item and the item's text as arguments.

[Note]Note

It may seem a waste to put our functions in tst_adding because we could also use them in tst_general and in other test cases. However, to keep the tutorial simple we will put the code in the tst_adding test case. It is of course very easy to create shared scripts, but we defer coverage of that to the user guide. (See How to Create and Use Shared Data and Shared Scripts (Section 15.4) for how to share scripts.)

If you look at the recorded test (tst_general) or in the Object Map you will see that Squish sometimes uses different names for the same things. The reason for this is that Squish needs to uniquely identify every object in a given context, and it uses whatever information it has to hand. However, this issue affects other GUI toolkits much more than it affects Tk.

Naturally, when we write test scripts we don't want to have to know or care which particular variation of a name to use, and Squish supports this need by providing alternative naming schemes, as we will see shortly.

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, 2010
Detail LookupError: Item 'New...' in object ':addressbook.tcl' not found or ready.
Called from: C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl: 18 
Location C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl: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 17.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.

So now we are ready to 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.

Tcl
proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 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]
    }
    verifyRows [llength $data]
    closeWithoutSaving
}

Python
def main():
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(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")]
    for oneNameAndAddress in data:
        addNameAndAddress(oneNameAndAddress)
    verifyRows(len(data))
    closeWithoutSaving()

JavaScript
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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]);
    verifyRows(data.length);
    closeWithoutSaving();
}

Perl
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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});
    }
    verifyRows(scalar(@data));
    closeWithoutSaving;
}

We begin by starting the application with a call to the startApplication function. The name we pass as a string is the name registered with Squish (normally the name of the executable).

The invokeMenuItem function is one we have created specially for this test. It takes a menu name and a menu option name and invokes the menu option. After using the invokeMenuItem function to do File|New, we verify that the table's row count is 0. The test.verify function is useful when we simply want to verify that a condition is true rather than compare two different values. (For Tcl we usually use the test.compare function rather than the test.verify function simply because it is slightly simpler to use in Tcl.)

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. Then we again compare the table's row count, this time to the number of rows in our sample data. And finally we call a custom closeWithoutSaving function to terminate the application.

We will now review each of the four supporting functions (and a fifth function that one of them uses), so as to cover all the code in the tst_adding test case, starting with the invokeMenuItem function.

Tcl
proc invokeMenuItem {menu item} {
    waitForObjectItem ":addressbook\\.tcl.#menuBar" $menu
    invoke activateItem ":addressbook\\.tcl.#menuBar" $menu
    set menuName [string tolower $menu]
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
    invoke activateItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
}

Python
def invokeMenuItem(menu, item):
    waitForObjectItem(":addressbook\\.tcl.#menuBar", menu)
    activateItem(":addressbook\\.tcl.#menuBar", menu)
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#%s" % menu.lower(), item)
    activateItem(":addressbook\\.tcl.#menuBar.#%s" % menu.lower(), item)

JavaScript
function invokeMenuItem(menu, item)
{
    waitForObjectItem(":addressbook\\.tcl.#menuBar", menu);
    activateItem(":addressbook\\.tcl.#menuBar", menu);
    var menuText = menu.toLowerCase();
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
    activateItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
}

Perl
sub invokeMenuItem
{
    my ($menu, $item) = @_;
    waitForObjectItem(":addressbook\\.tcl.#menuBar", $menu);
    activateItem(":addressbook\\.tcl.#menuBar", $menu);
    my $menuText = lc $menu;
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#$menuText", $item);
    activateItem(":addressbook\\.tcl.#menuBar.#$menuText", $item);
}

Symbolic names always begin with a colon and embed various bits of information about an object and its type. Real names are represented by a brace enclosed list of space-separated key–value pairs. Every real name must specify the type property and at least one other property. Here we've used the symbolic names that Squish generated, except that for the menu item we paramaterize the symbolic name with the menu item's (lower-cased) text.

Once we have identified the object we want to interact with we use the waitForObjectItem function to retrieve a reference to it and in this case we then apply the activateItem function to it. The waitForObjectItem function pauses Squish until the specified object and its item are visible and enabled. So, here, we waited for the menu bar and one of its menu bar items, and then we waited for a menu bar item and one of its menu items. And as soon as the waiting is over each time we activate the object and its item using the activateItem function.

Tcl
proc verifyRows {expected_rows} {
    waitForObject ":addressbook\\.tcl.view.tree"
    set rows [invoke tcleval ".view.tree size"]
    test compare $rows "$expected_rows"
}

Python
def verifyRows(expected_rows):
    waitForObject(":addressbook\\.tcl.view.tree")
    rows = tcleval(".view.tree size")
    test.verify(cast(rows, int) == expected_rows)

JavaScript
function verifyRows(expected_rows)
{
    waitForObject(":addressbook\\.tcl.view.tree");
    var rows = tcleval(".view.tree size");
    test.verify(parseInt(rows) == expected_rows);
}

Perl
sub verifyRows
{
    my $expected_rows = shift;
    waitForObject(":addressbook\\.tcl.view.tree");
    my $rows = tcleval(".view.tree size");
    test::verify($rows eq $expected_rows);
}

Rather than duplicate the three lines needed to verify the row count in two separate places we have packaged the functionality up into a tiny function. (Note that for the Python version we used the cast function since Squish has its own int object; see also, Python Modules (Section 16.1.13.1).)

Tcl
proc addNameAndAddress {oneNameAndAddress} {
    invokeMenuItem "Edit" "Add..."
    set fieldNames [list "forename" "surname" "phone" "email"]
    for {set field 0} {$field < [llength $fieldNames]} {incr field} {
        set fieldName [lindex $fieldNames $field]
        set text [lindex $oneNameAndAddress $field]
        enterText ":addressbook\\.tcl.dialog.$fieldName" $text
    }
    invoke clickButton [waitForObject ":addressbook\\.tcl.dialog.buttonarea.ok"]
}

Python
def addNameAndAddress(oneNameAndAddress):
    invokeMenuItem("Edit", "Add...")
    for fieldName, text in zip(("forename", "surname", "phone", "email"), oneNameAndAddress):
        enterText(":addressbook\\.tcl.dialog.%s" % fieldName, text)
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"))

JavaScript
function addNameAndAddress(oneNameAndAddress)
{
    invokeMenuItem("Edit", "Add...");
    var fieldNames = new Array("forename", "surname", "phone", "email");
    for (var i = 0; i < oneNameAndAddress.length; ++i) {
        var fieldName = fieldNames[i];
        var text = oneNameAndAddress[i];
        enterText(":addressbook\\.tcl.dialog." + fieldName, text);
    }
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
}

Perl
sub addNameAndAddress
{
    my (@oneNameAndAddress) = @_;
    invokeMenuItem("Edit", "Add...");
    my @fieldNames = ("forename", "surname", "phone", "email");
    my $fieldName = "";
    for (my $i = 0; $i < scalar(@fieldNames); $i++) {
        $fieldName = $fieldNames[$i];
        my $text = $oneNameAndAddress[$i];
        enterText(":addressbook\\.tcl.dialog.$fieldName", $text);
    }
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
}

For each set of name and address data we invoke the Edit|Add menu option to pop up the Add dialog. Then for each value received we populate the appropriate field by waiting for the relevant line edit to be ready and then typing in the text using the type function. Entering text isn't completely straightforward, so we have created the enterText helper function that takes a line edit control and the text to enter as arguments and enters the text for us. And at the end we click the dialog's OK button. We copied the code for clicking the OK button from the tst_general test case's code.

Tcl
proc enterText {control text} {
    set items [list]
    for {set index 0} {$index < [string length $text]} {incr index} {
        set c [string index $text $index]
        if {[string is upper $c]} {
            lappend items "<Shift_L>" $c
        } elseif {[string is space $c]} {
            lappend items "<space>"
        } elseif {[string equal $c "."]} {
            lappend items "<period>"
        } elseif {[string equal $c "@"]} {
            lappend items "<Shift_L>" "<at>"
        } else {
            lappend items $c
        }
    }
    set shifted 0
    for {set index 0} {$index < [llength $items]} {incr index} {
        set item [lindex $items $index]
        if {$shifted} {
            invoke type [waitForObject $control] $item 17
            set shifted 0
        } else {
            invoke type [waitForObject $control] $item
        }
        if {[string equal $item "<Shift_L>"]} {
            set shifted 1
        }
    }
}

Python
def enterText(control, text):
    items = []
    for c in text:
        if c.isupper():
            items += ["<Shift_L>", c]
        elif c.isspace():
            items += ["<space>"]
        elif c == ".":
            items += ["<period>"]
        elif c == "@":
            items += ["<Shift_L>", "<at>"]
        else:
            items += [c]
    shifted = False
    for item in items:
        if shifted:
            type(waitForObject(control), item, 17)
            shifted = False
        else:
            type(waitForObject(control), item)
            if item == "<Shift_L>":
                shifted = True

JavaScript
function enterText(control, text)
{
    var isupper = /^[A-Z]$/;
    var isspace = /^\s$/;
    var items = new Array();
    for (var i = 0; i < text.length; ++i) {
        var c = text.charAt(i);
        if (isupper.test(c)) {
            items.push("<Shift_L>");
            items.push(c);
        }
        else if (isspace.test(c)) {
            items.push("<space>");
        }
        else if (c == ".") {
            items.push("<period>");
        }
        else if (c == "@") {
            items.push("<Shift_L>");
            items.push("<at>");
        }
        else {
            items.push(c);
        }
    }
    var shifted = false;
    for (i in items) {
        item = items[i];
        if (shifted) {
            type(waitForObject(control), item, 17);
            shifted = false;
        }
        else {
            type(waitForObject(control), item);
            if (item == "<Shift_L>") {
                shifted = true;
            }
        }
    }
}

Perl
sub enterText
{
    my ($control, $text) = @_;
    my @items;
    foreach $c (split //, $text) {
        if ($c =~ /^[A-Z]$/) {
            push @items, "<Shift_L>", $c;
        }
        elsif ($c =~ /^\s$/) {
            push @items, "<space>";
        }
        elsif ($c eq ".") {
            push @items, "<period>";
        }
        elsif ($c eq "@") {
            push @items,"<Shift_L>", "<at>";
        }
        else {
            push @items, $c;
        }
    }
    my $shifted = 0;
    foreach my $item (@items) {
        if ($shifted) {
            type(waitForObject($control), $item, 17);
            $shifted = 0;
        }
        else {
            type(waitForObject($control), $item);
            if ($item eq "<Shift_L>") {
                $shifted = 1;
            }
        }
    }
}

Squish handles some characters specially when typing them into an AUT, so we must account for this. In particular, periods (.) and at symbols (@) must be replaced by named characters, and some characters must be shifted—in which case we must type shift, then the character, then release the shift.

We begin by iterating over all the characters in the text and creating an equivalent list of characters and special strings. Then we iterate over this list and type in each item—and making sure to release the shift (by passing an extra argument of 17) if we have just entered a shifted character.

We got the line that calls the type function at the heart of the enterText function by copying it from the recorded tst_general test and simply parameterizing it by the field name (actually done in the addNameAndAddress calling function), and text.

Python
def closeWithoutSaving():
    invokeMenuItem("File", "Quit")
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"))

JavaScript
function closeWithoutSaving()
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"));
}

Perl
sub closeWithoutSaving
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject(":addressbook\\.tcl.__tk__messagebox.no"));
}

Tcl
proc closeWithoutSaving {} {
    invokeMenuItem "File" "Quit"
    invoke clickButton [waitForObject ":addressbook\\.tcl.__tk__messagebox.no"]
}

Here we use the invokeMenuItem function to do File|Quit, and then click the "save unsaved changes" dialog's No button. The last line was copied from the recorded test.

The entire test is around 75 lines of code—and would be even less if we put some of the common functions (such as invokeMenuItem, enterText, verifyRows, and closeWithoutSaving) 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 for Test Scripts (Section 16.1) in the Tools Reference Manual (Chapter 16)). 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.

12.4.2. Creating Data Driven Tests

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). [11]

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.tcl application we want to import the MyAddresses.tsv data file. To do this we must start by clicking File|Import Test Resource to pop-up the "Import Squish Resource" dialog (Section 17.3.3). Inside the dialog click the Browse 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 Copy to Test Suite for Sharing radio button. Now click the Finish 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 17.2.5). The screenshot shows Squish after the test data has been added.

[Note]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 shared. Now make a directory inside the shared directory called testdata. Now copy the data file (in this example, MyAddresses.tsv) into the shared\testdata directory. Now quit the Squish IDE if it is running and start it up again. If you click the Test Suite Resources view's Test Data tab you should see the data file. Click the file's name to see it in an Editor view (Section 17.2.5).

Squish with some imported test data

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.

Tcl
proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 0
    # Set a limit to avoid testing 100s of rows
    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 phone [testData field $record "Phone"]
        set email [testData field $record "Email"]
        set details [list $forename $surname $phone $email]
        addNameAndAddress $details
        checkNameAndAddress $record
        if {$row >= $limit} {
            break
        }
    }
    verifyRows [expr $row + 1]
    closeWithoutSaving
}

Python
def main():
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
        forename = testData.field(record, "Forename")
        surname = testData.field(record, "Surname")
        phone = testData.field(record, "Phone")
        email = testData.field(record, "Email")
        addNameAndAddress((forename, surname, phone, email)) # pass as a single tuple
        checkNameAndAddress(record)
        if row >= limit:
            break
    verifyRows(row + 1)
    closeWithoutSaving()

JavaScript
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    var limit = 10; // To avoid testing 100s of rows since that would be boring
    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 phone = testData.field(record, "Phone");
        var email = testData.field(record, "Email");
        addNameAndAddress(new Array(forename, surname, phone, email));
        checkNameAndAddress(record);
        if (row >= limit)
            break;
    }
    verifyRows(row + 1);
    closeWithoutSaving();
}

Perl
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(0);
    my $limit = 10; # To avoid testing 100s of rows since that would be boring
    my @records = testData::dataset("MyAddresses.tsv");
    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 $phone = testData::field($record, "Phone");
        my $email = testData::field($record, "Email");
        addNameAndAddress($forename, $surname, $phone, $email);
        checkNameAndAddress($record);
        if ($row >= $limit) {
            last;
        }
    }
    verifyRows($row + 1);
    closeWithoutSaving;
}

Having used the test data to populate 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.

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.

Tcl
proc checkNameAndAddress {record} {
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set expected_text [testData field $record $column]
        waitForObject ":addressbook\\.tcl.view.tree"
        # New items are always inserted before the current one, so the row is always 0
        set cell [toString [invoke tcleval ".view.tree cellindex 0,$column"]]
        set actual_text [invoke tcleval ".view.tree cellcget $cell -text"]
        test compare $expected_text $actual_text
    }
}

Python
def checkNameAndAddress(record):
    for column in range(len(testData.fieldNames(record))):
        expected_text = testData.field(record, column)
        waitForObject(":addressbook\\.tcl.view.tree")
        # New items are always inserted before the current one, so the row is always 0
        actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,%d] -text" % column)
        test.compare(expected_text, actual_text)

JavaScript
function checkNameAndAddress(record)
{
    for (var column = 0; column < testData.fieldNames(record).length; ++column) {
        var expected_text = testData.field(record, column);
        waitForObject(":addressbook\\.tcl.view.tree");
        var actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0," + column + "] -text");
        test.compare(expected_text, actual_text);
    }
}

Perl
sub checkNameAndAddress
{
    my $record = shift;
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        my $expected_text = testData::field($record, $column);
        waitForObject(":addressbook\\.tcl.view.tree");
        # New items are always inserted before the current one, so the row is always 0
        my $actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,$column] -text");
        test::compare($expected_text, $actual_text);
    }
}

This function accesses the table's first row and extracts each of its columns' values. 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. Note that for this particular test we always insert new rows at the start of the table. The effect of this is that every new name and address is always added as the first row, so this is why we hard-coded the row to be 0.

The screenshot show Squish's Test Summary log after the data-driven tests have been run.

Squish after a successful data-driven test run



[11] Both .csv and .tsv files are assumed to use the Unicode UTF-8 encoding—the same encoding used for all test scripts.