15.4. How to Create and Use Shared Data and Shared Scripts

15.4.1. How to Store and Locate Shared Scripts and Shared Data Files
15.4.2. How to Do Data-Driven Testing
15.4.3. How to Use Test Data in the AUT

This section discusses how to split tests into multiple files and how to share, access and use, shared scripts and shared data files.

15.4.1. How to Store and Locate Shared Scripts and Shared Data Files

Each test case contains a default script file called test.<lang> (where <lang> is one of py, js, pl or tcl).

When creating test suites it often happens that many of the test cases require some common functionality. Squish makes it possible to create separate script files that contain the common functionality and which can be used by all the test cases that need it.

Shared scripts are located in the test suite's shared/scripts folder.

To create a shared script, in the Squish IDE's test suite view, right click on scripts under shared, and choose the New Script option from the context menu.

Then enter the name of the shared script file, and press Enter. Once this is done you can edit the shared script in the Squish IDE, and you can access its functionality in the test suite's test case scripts.

If you want to import an existing script into the test suite's shared script's folder, just choose the Import Script option in the context menu instead.

Once a shared script has been created, it can be used by any test-case-specific scripts that need it. Note though, that shared scripts are not usually imported using the language-specific import mechanism—after all, not all languages have such a mechanism, for example, JavaScript doesn't. Instead, the Squish API provides the necessary functions.

The standard way to locate a shared script file (or shared data file), is to use the findFile() function. The first argument is the type of file, which for shared scripts should be scripts. The second argument is the script's filename (with no path). The function will search all the standard locations that Squish uses (shared/scripts and SQUISH_SCRIPT_DIR), and will return the filename including its full path.

Once we have the full path to the shared script, we can include it in our test case's script. This is done by evaluating the shared script using the source() function—this means that it is in effect executed as if the actual text of the shared script was in the test case at the point where we call the source() function. After this is done, all the objects created in the shared script—typically, classes and functions—become accessible in the test case's script.

Here is an example where we want to share a script file called address_utility.py (or address_utility.js, etc., depending on the scripting language being used), so that we can access a function inside it—in this example, insertDummyNamesAndAddresses()—that populates an addressbook AUT with some names and addresses so that there is some data present for further tests to work on.

Python
def main():
    ...
    source(findFile("scripts", "address_utility.py"))
    insertDummyNamesAndAddresses()
    ...
JavaScript
function main()
{
    // ...
    source(findFile("scripts", "address_utility.js"));
    insertDummyNamesAndAddresses();
    // ...
}
Perl
sub main
{
    # ...
    source(findFile("scripts", "address_utility.pl"));
    insertDummyNamesAndAddresses();
    # ...
}
Tcl
proc main {} {
    # ...
    source [findFile scripts "address_utility.tcl"]
    insertDummyNamesAndAddresses
    # ...
}

Here we import the address_utility.py (or similar) script which defines a function called insertDummyNamesAndAddresses(), which we are then able to call. (You should already be familiar with this, since we used this mechanism in several examples earlier in this User Guide.)

For scripting languages such as Python that support importing, it is possible to use the language's standard import mechanism. However, Squish's approach is usually more convenient, since in most cases our shared scripts are only relevant to our tests.

Just as we have a test case script, we can also have test-case-specific data files. Such files are stored in the test case's testdata directory. However, in some cases, we want to share the test data so that more than one test case can access it. In this case we store the test data in the test suite's shared/testdata directory.

Test data can be added through the Squish IDE by importing files—Squish can read .tsv (tab-separated values format), .csv (comma-separated values format), and .xls (Microsoft® Excel™ spreadsheet format). Or we can simply create the directories on the command line in a console and copy our test data into them. The techniques used for adding shared test data, whether using the Squish IDE or manually, are exactly the same as for adding shared test scripts, only we use the appropriate testdata directory (rather than the scripts directory).

Although the top-level directory structure must follow what we have described, within that structure—i.e., under a testdata directory—you are free to create subdirectories and structure them however you like.

Retrieving test data is done using the findFile() function we mentioned earlier, only this time the first argment must be testdata, and the second argument the name of the test data file you want to access. Test data is normally retrieved using Squish's data handling API; see Test Data Handling (Section 16.1.3.8).

15.4.2. How to Do Data-Driven Testing

Data-driven testing is an approach where the test data (input, and expected output), is kept separate from the test script code which only contains the test's logic. The normal practice is for the test data to be read from a file or database one item or record at a time, and for the test script to use the data to test the AUT and then compare the results with those that are expected.

One benefit of this approach is that it makes it possible to modify a test without actually having to change the test case's code—instead we simply add more data which the test then reads and processes along with the rest of the data. This makes it possible to separate responsibility for creating tests between test engineers who have coding skills and those who don't. Those with coding skills can create test scripts and encode the test logic in them, and test engineers who don't have coding skills can create and edit the test data that the test scripts use to test the AUT.

Squish provides an API for handling test data (see Test Data Handling (Section 16.1.3.8)), that makes it easy to create data-driven tests. Importing test data files was been discussed in the previous section—here we will see how to use Squish's script API to read and use test data, and will assume that the test data has already be imported or copied into the appropriate testdata directory.

Test data always contains data in a tabular format. Squish can read files in .tsv (tab-separated values format), .csv (comma-separated values format), and .xls (Microsoft® Excel™ spreadsheet format). In the case of .csv and .tsv files, Squish assumes that they use the Unicode UTF-8 encoding—the same encoding that is used for test scripts. In .tsv files, records are separated by new lines and fields are separated by tabs. The first record is used to describe the columns. Here is an example .tsv data file—addresses.tsv—with tabs indicated by -> and newlines indicated by $ characters:

First Name->Last Name->Address->Email->Number$
Max->Mustermann->Bakerstreet 55->max@mustermann.net->1$
John->Kelly->Rhodeo Drv. 678->jkelly@acompany.com->2$
Joe->Smith->Queens Blvd. 37->joe@smith.com->3$

Each field (column) is separated by a tab, and each record (row, or line) is separated by a newline. As is common practice with .tsv (and .csv) files, the first line is not data as such, but instead the field (column) names (First Name, Last Name, etc.). All the other lines are data records.

Here is an example where we read each record in turn and print its values to Squish's log:

Python
for record in testData.dataset("addresses.tsv"):
    firstName = testData.field(record, "First Name")
    lastName = testData.field(record, "Last Name")
    address = testData.field(record, "Address")
    email = testData.field(record, "Email")

    test.log("%s %s, %s; email: %s" % (firstName, lastName, address, email))
JavaScript
var records = testData.dataset("addresses.tsv");
for (var row = 0; row < records.length; ++row) {
    var record = records[row];
    firstName = testData.field(record, "First Name");
    lastName = testData.field(record, "Last Name");
    address = testData.field(record, "Address");
    email = testData.field(record, "Email");

    test.log(firstName + " " + lastName + ", " + address + "; email:" + email);
}
Perl
my @records = testData::dataset("addresses.tsv");
for (my $row = 0; $row < scalar(@records); $row++) {
    my $record = $records[$row];
    my $firstName = testData::field($record, "First Name");
    my $lastName = testData::field($record, "Last Name");
    my $address = testData::field($record, "Address");
    my $email = testData::field($record, "Email");

    test::log("$firstName $lastName, $address; email: $email");
}
Tcl
set records [testData dataset "addresses.tsv"]
for {set row 0} {$row < [llength $data]} {incr row} {
    set record [lindex $records $row]
    set firstName [testData field $record "First Name"]
    set lastName [testData field $record "Last Name"]
    set address [testData field $record "Address"]
    set email [testData field $record "Email"]

    test log "$firstName $lastName, $address; email: $email"
}

Notice that we access fields by name, so the names we use in our test case's code must match those in the first line of the test data file.

In typical cases the test data file is found using the findFile() function, and then the records are retrieved using the testData.dataset() function. We then use the testData.field() function to access the contents of individual fields within a given record.

By using a for loop we can iterate over every record in the testdata—without having to know in advance how many records there are, so the code is unaffected if records are removed or added. And of course, in a realistic test we would feed the data to the AUT and compare expected with actual results rather than simply printing the data to the log as we have done here.

Most of the tutorials include a complete example of a data-driven test. For a Qt example, see Creating Data Driven Tests (Section 5.4.2); for a Java AWT/Swing example, see Creating Data Driven Tests (Section 6.4.2); for a Java SWT example, see Creating Data Driven Tests (Section 7.4.2); and for a Tk/Tcl example, see Creating a Data-Driven Test (Section 13.4).

15.4.3. How to Use Test Data in the AUT

So far this section has only discussed using test data in test scripts to create data driven tests. But two other use cases arise in practice. One use case is where test data files are provided for the AUT to read, and another is where we want to retrieve files that the AUT has created during the test run so that they can be verified.

Let's start by looking at the first use case where we provide a data file for the AUT to read. For example, imagine we are using the addressbook AUT and we want it to load a file called customers.adr at the start of the test script so that it has a known set of data to work on. This is easily achieved by storing the data file in the test case's testdata directory—or in the test suite's testdata directory, if we want more than one test case to be able to access it.

We want to avoid hard-coding the path to the data file in our test script since we want the flexibility to run our tests on different machines, and even on different platforms. We can copy a data file into the AUT's current working directory (without having to know its path) using the testData.put() function. We only need to give the name of the file to this function since it will automatically look in the test case's (and if not there, the test suite's), testdata directory. Another benefit of using this function is that once the test run has finished, Squish will automatically clean up for us (i.e., Squish will delete the file from the AUT's working directory).

Here is an example where we copy a test data file from the AUT's testdata directory into the AUT's working directory. Then we access the AUT's main window object (Addressbook), and call that object's fileOpen() method with the name of the file we want it to load.

Python
def main():
    ...
    testData.put("customers.adr")
    findObject("Addressbook").fileOpen("customers.adr")
    ...
JavaScript
function main()
{
    // ...
    testData.put("customers.adr");
    findObject("Addressbook").fileOpen("customers.adr");
    // ...
}
Perl
sub main()
{
    # ...
    testData::put("customers.adr");
    findObject("Addressbook")->fileOpen("customers.adr");
    # ...
}
Tcl
proc main {} {
    # ...
    invoke testData put "customers.adr"
    [invoke [invoke findObject "Addressbook"] fileOpen "customers.adr"]
    # ...
}

We don't have to insert calls to the testData.put() function ourselves when we record test scripts, since we can tell Squish to insert them for us. This can be done by going to the Test Data tab in the Record Settings dialog. This tab lists the available test data files with an unchecked checkbox beside each one—we can simply check the checkboxes of those files for which we want the recorded script to include testData.put() function calls at the start of the test script.

Another use case is where we need to verify that a file which has been created by the AUT has the contents we expect. For example, let's assume that during a test the addressbook AUT loads a data file, customers.adr, performs various operations (adds, edits, and deletes, addresses), and then saves its current address data into a new file, edited-customers.adr. After this has happened we want our test script to compare the edited-customers.adr file with another file, expected-customers.adr which has the contents we expect the file to have after accounting for the changes to the data made earlier in the script.

We can retrieve a file from the AUT's current directory using the testData.get() function. Here's an example that saves a file, retrieves the saved file and a file that should match the saved file, and verifies that they match:

Python
def main():
    # load in customers.adr and add/edit/delete addresses

    mainwindow = waitForObject("Addressbook")
    mainwindow.saveAs("edited-customers.adr")

    testData.get("edited-customers.adr")
    data1 = open(findFile("testdata" "edited-customers.adr")).read()
    data2 = open(findFile("testdata" "expected-customers.adr")).read()
    test.compare(data1, data2)
JavaScript
function main()
{
    // load in customers.adr and add/edit/delete addresses

    var mainwindow = waitForObject("Addressbook");
    mainwindow.saveAs("edited-customers.adr");

    testData.get("edited-customers.adr");
    var data1 = File.open(findFile("testdata" "edited-customers.adr")).read();
    var data2 = File.open(findFile("testdata" "expected-customers.adr")).read();
    test.compare(data1, data2);
}
Perl
sub main
{
    # load in customers.adr and add/edit/delete addresses

    my $mainwindow = waitForObject("Addressbook");
    $mainwindow->saveAs("edited-customers.adr");

    testData::get("edited-customers.adr");
    open(FH1, findFile("testdata" "edited-customers.adr"));
    open(FH2, findFile("testdata" "expected-customers.adr"));
    my $data1 = join("", <F1>);
    my $data2 = join("", <F2>);
    test.compare($data1, $data2);
}
Tcl
proc main {} {
    # load in customers.adr and add/edit/delete addresses

    set mainwindow [waitForObject "Addressbook"]
    invoke $mainwindow saveAs "edited-customers.adr"

    testData get "edited-customers.adr"
    set data1 [read [open [findFile testdata "edited-customers.adr]]]
    set data2 [read [open [findFile testdata "expected-customers.adr]]]
    test compare $data1 $data2
}

We begin by getting a reference to the AUT's main window object; then we call the main window's saveAs() method to save the edited data. Then we open both the newly saved data and the expected data files, and read their entire contents. Finally, we compare the contents of the files using the test.compare() function.

Squish's data handling API (see Test Data Handling (Section 16.1.3.8)) has other useful functions. For example, if we only need to test that a file has been created without concern for its contents we can call the testData.exists() function. And if we want to remove a file in the course of a test we can call the testData.remove() function.