How to Do Keyword-Driven Testing

Keyword-driven testing (also called "table-driven testing" and "action-word testing") is a testing methodology whereby tests are driven wholly by data. What makes keyword-driven testing different from data-driven testing is that in the latter we just read in data items, for example, to populate a GUI table, but in the former the data items aren't just data but the names of AUT-specific functions and their arguments which are then executed as the test runs.

The great advantage of keyword-driven testing is that the tests can be created purely as data tables in terms of high-level AUT actions such as "Add Item" or "Delete Item" that testers familiar with the AUT can relate to without having to know the more technical aspects under the hood.

The creation of keyword-driven tests involves two phases. First comes the one-off creation of some AUT-specific test script functions to interpret the data and of a generic "driver" function that reads test data from a data source and that executes the AUT-specific test script functions based on the data. The second is the creation of one or more test cases and corresponding data tables that are used to drive the tests.

In this section we will first look at how a tester goes about creating a test case and the corresponding test data, as well as the results this produces, and then we will look at the one-off work that must be done behind the scenes to make it all work.

All the examples shown in this section are in Squish's examples directory (SQUISHDIR/examples/qt/addressbook/suite_keyword_py for the Python version, SQUISHDIR/examples/qt/addressbook/suite_keyword_js for the JavaScript version, and so on). The test case itself is called tst_keyword_driven.

Although we have used a Qt-based AUT the underlying GUI toolkit doesn't matter—using the ideas shown in this section you could create keyword-driven testing for any toolkit that Squish supports.

How to Create a Keyword-Driven Test

The test case needed for keyword-driven tests is always the same—and incredibly simple:

source(findFile("scripts", "driver.py"))

def main():
    drive("keywords.tsv")
source(findFile("scripts", "driver.js"));

function main()
{
    drive("keywords.tsv");
}
source( findFile( "scripts", "driver.pl" ) );

sub main {
    drive("keywords.tsv");
}
def main
  require findFile("scripts", "driver.rb")
  drive("keywords.tsv")
end
source [findFile "scripts" "driver.tcl"]

proc main {} {
    drive "keywords.tsv"
}

First the test program loads in the generic "driver" functionality and then it executes the drive() function on a test data file. The test data file specifes exactly what actions should be taken, beginning with starting the AUT and ending with terminating the AUT.

Here is what a typical (but rather small) data file might look like:

Keyword→Argument 1→Argument 2→Argument 3→Argument 4
startApplication→addressbook
chooseMenuItem→File→New
verifyRowCount→0
addAddress→Red→Herring→red.herring@froglogic.com→555 123 4567
addAddress→Blue→Cod→blue.cod@froglogic.com→555 098 7654
addAddress→Green→Pike→green.pike@froglogic.com→555 675 8493
verifyRowCount→3
removeAddress→green.pike@froglogic.com
removeAddress→blue.cod@froglogic.com
removeAddress→red.herring@froglogic.com
verifyRowCount→0
terminate

(We have indicated tab-separators using the → character.) The first row contains the field names, the other rows contain the actions to be performed. In each action row the first column contains the name of a high-level AUT-specific function to execute, and the remaining columns contain any arguments that the function might require.

Here, the terminate function requires no arguments and the verifyRowCount and removeAddress functions both require one argument (the former a row count to verify, and the latter an email address to identify the row to be deleted). Similarly, the chooseMenuItem function always requires two arguments (the name of the menu and the name of the menu's menu item) and the addAddress function requires four arguments (forename, surname, email, phone).

Here are the results produced by a typical test run (with dates and most items elided):

Start tst_keyword_driven
Log Drive: 'keywords.tsv'
Log Execute: startApplication('addressbook')
Log Execute: chooseMenuItem('File', 'New')
Log Execute: verifyRowCount('0')
Pass Verified
Log Execute: addAddress('Red', 'Herring', 'red.herring@froglogic.com',
'555 123 4567')
Pass Verified
Pass Comparison
Pass Comparison
Pass Comparison
Pass Comparison
...
Log Execute: verifyRowCount('3')
Pass Verified
Log Execute: removeAddress('green.pike@froglogic.com')
Log Removed green.pike@froglogic.com
Pass Verified
...
Log Execute: verifyRowCount('0')
Pass Verified
Log Execute: terminate()

It should be obvious that we can easily add as many keyword actions as we like—quite independently of the test case script which will simply carry out whatever tests are specified in the data file. And, of course, if additional actions were required—such as an editAddress function—there's no reason why that couldn't be added as an AUT-specific function, and once added it could then be used in the data file along with all the rest.

So, from the point of view of a tester who wants to do keyword-driven testing, the job is straightforward. However, for this simplicity to be achieved requires the writing of the AUT-specific functions that the test data needs to execute, along with the generic driver function. These are covered in the next two subsections.

How to Create AUT-Specific Support for Keyword Driven Tests

Every keyword action specified in the test data must have a corresponding function that takes the given arguments and performs the given actions. There is no generic solution for this because each action will be AUT-specific. Nonetheless, for completeness, we will show the implementations of all the actions used in the test data shown above, with one exception. The exception is the ApplicationContext startApplication(autName) function which is already built into Squish so we don't need to implement it ourselves.

The following functions are all taken from the action.py (or action.js and so on), script.

def chooseMenuItem(menu, item):
    activateItem(waitForObjectItem(names.address_Book_QMenuBar, menu))
    activateItem(waitForObjectItem(menu, item))
function chooseMenuItem(menu, item)
{
    activateItem(waitForObjectItem(names.addressBookQMenuBar, menu));
    activateItem(waitForObjectItem(menu, item));
}
sub chooseMenuItem {
    my ( $menu, $item ) = @_;
    activateItem( waitForObjectItem( $Names::address_book_qmenubar, $menu ) );
    activateItem(
        waitForObjectItem($menu, $item ) );
}
def chooseMenuItem(menu, item)
  activateItem(waitForObjectItem(Names::Address_Book_QMenuBar, menu))
  activateItem(waitForObjectItem(menu, item))
end
proc chooseMenuItem {menu item} {
    invoke activateItem [waitForObjectItem $names::Address_Book_QMenuBar $menu]
    invoke activateItem [waitForObjectItem $menu $item]
}

This function activates the given menu and then the given menu item. We have used symbolic names which we got from the Object Map.

Of course, the Object Map starts out empty, so the first thing we did—before creating any functions—was to record and play back a dummy test. In that test we did everything we planned to do in the keyword driven test (but with no repetitions). So we created a new file, added an address, removed an address, and quit. This populated the Object Map with most of the names we need.

The menu bar and menu options had more than one symbolic name referring to them because as the AUT's state changed, its window title changed, and Squish kept track of this by creating multiple symbolic names, each with a different window property value. This property's value varied depending on the window's title, which itself varied. We need to be able to access the menu bar and menu items regardless of the AUT's state (and in particular, regardless of its window title).

To solve the problem we used the Object Map view to edit the Object Map. First we we deleted the window property from the ":Address Book_QMenuBar" item so that the menu bar could be found no matter what the window's title. Then we deleted the window property from the ":Address Book.File_QMenu" item for the same reason. Then we copied the ":Address Book.File_QMenu" symbolic name and pasted it; we renamed the pasted version ":Address Book.Edit_QMenu" and changed its title property's value from "File" to "Edit". We then deleted any menu items that contained "Unnamed" in their symbolic names.

After editing the Object Map this function for invoking a menu and then one of its menu options works perfectly—irrespective of the AUT's window title.

def verifyRowCount(rows):
    test.verify(__builtin__.int(rows) == getRowCount(), "row count")
function verifyRowCount(rows)
{
    rows = parseInt(rows)
    test.verify(rows == getRowCount(), "row count");
}
sub verifyRowCount {
    my $rows = shift;
    test::verify( $rows eq getRowCount(), "row count" );
}
def verifyRowCount(rows)
  Test.verify(rows.to_i == getRowCount(), "row count")
end
proc verifyRowCount {rows} {
    test compare $rows [getRowCount] "row count"
}

This function is used to verify that the number of rows in the AUT are what we expect.

All of the keyword data is stored and retrieved as strings. We could, of course, use two columns for data items (type, value), or some other type-specifying scheme (such as type=value) items. But this pushes the burden of dealing with different types on the tester. So instead we accept everything in the form of strings and where we need other types, as here, we perform the conversion in the AUT-specific functions. (Except for Perl where we simply force a string comparison.)

For Python the conversion is slightly complicated by the fact that Squish imports its own Python-specific int() function that's different from the built-in one. To work around this we import Python's __builtin__ (Python 2) or builtins (Python 3) module and access Python's own int() conversion function to turn the rows string into a number (See Python Notes.)

def getRowCount():
    tableWidget = waitForObject(
            names.address_Book_Unnamed_File_QTableWidget)
    return tableWidget.rowCount
function getRowCount()
{
    tableWidget = waitForObject(
        names.addressBookUnnamedFileQTableWidget);
    return tableWidget.rowCount;
}
sub getRowCount {
    my $tableWidget =
      waitForObject($Names::address_book_unnamed_file_qtablewidget);
    return $tableWidget->rowCount;
}
def getRowCount
  tableWidget = waitForObject(
  Names::Address_Book_Unnamed_File_QTableWidget)
  tableWidget.rowCount
end
proc getRowCount {} {
    set tableWidget [waitForObject \
            $names::Address_Book_Unnamed_File_QTableWidget]
    return [property get $tableWidget rowCount]
}

This helper function retrieves a reference to the underlying toolkit's table (in this case a QTableWidget), and then returns the value of its rowCount property. The table's symbolic name was copied from the Object Map after the dummy test run had populated the Object Map.

def addAddress(forename, surname, email, phone):
    oldRowCount = getRowCount()
    chooseMenuItem("Edit", "Add...")
    type(waitForObject(names.forename_LineEdit), forename)
    type(waitForObject(names.surname_LineEdit), surname)
    type(waitForObject(names.email_LineEdit), email)
    type(waitForObject(names.phone_LineEdit), phone)
    clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
    newRowCount = getRowCount()
    test.verify(oldRowCount + 1 == newRowCount, "row count")
    row = oldRowCount # The first item is inserted at row 0;
    if row > 0:       # subsequent ones at row rowCount - 1
        row -= 1
    checkTableRow(row, forename, surname, email, phone)
function addAddress(forename, surname, email, phone)
{
    var oldRowCount = getRowCount();
    chooseMenuItem("Edit", "Add...");
    type(waitForObject(names.forenameLineEdit), forename);
    type(waitForObject(names.surnameLineEdit), surname);
    type(waitForObject(names.emailLineEdit), email);
    type(waitForObject(names.phoneLineEdit), phone);
    clickButton(waitForObject(names.addressBookAddOKQPushButton));
    var newRowCount = getRowCount();
    test.verify(oldRowCount + 1 == newRowCount, "row count");
    var row = oldRowCount // The first item is inserted at row 0;
    if (row > 0) {        // subsequent ones at row rowCount - 1
        --row;
    }
    checkTableRow(row, forename, surname, email, phone);
}
sub addAddress {
    my ( $forename, $surname, $email, $phone ) = @_;
    my $oldRowCount = getRowCount();
    chooseMenuItem( "Edit", "Add..." );
    type( waitForObject($Names::forename_lineedit), $forename );
    type( waitForObject($Names::surname_lineedit),  $surname );
    type( waitForObject($Names::email_lineedit),    $email );
    type( waitForObject($Names::phone_lineedit),    $phone );
    clickButton( waitForObject($Names::address_book_add_ok_qpushbutton) );
    my $newRowCount = getRowCount();
    test::verify( $oldRowCount + 1 == $newRowCount, "row count" );
    my $row = $oldRowCount;    # The first item is inserted at row 0

    if ( $row > 0 ) {          # subsequent ones at row rowCount - 1
        --$row;
    }
    checkTableRow( $row, $forename, $surname, $email, $phone );
}
def addAddress(forename, surname, email, phone)
  oldRowCount = getRowCount()
  chooseMenuItem("Edit", "Add...")
  type(waitForObject(Names::Forename_LineEdit), forename)
  type(waitForObject(Names::Surname_LineEdit), surname)
  type(waitForObject(Names::Email_LineEdit), email)
  type(waitForObject(Names::Phone_LineEdit), phone)
  clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
  newRowCount = getRowCount()
  Test.verify(oldRowCount + 1 == newRowCount, "row count")
  row = oldRowCount # The first item is inserted at row 0;
  if row > 0        # subsequent ones at row rowCount - 1
    row -= 1
  end
  checkTableRow(row, forename, surname, email, phone)
end
proc addAddress {forename surname email phone} {
    set oldRowCount [getRowCount]
    chooseMenuItem "Edit" "Add..."
    invoke type [waitForObject $names::Forename_LineEdit] $forename
    invoke type [waitForObject $names::Surname_LineEdit] $surname
    invoke type [waitForObject $names::Email_LineEdit] $email
    invoke type [waitForObject $names::Phone_LineEdit] $phone
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]
    set newRowCount [getRowCount]
    test compare [expr {$oldRowCount + 1}] $newRowCount "row count"
    set row $oldRowCount
    if {$row > 0} {
        set row [expr {$row - 1}]
    }
    checkTableRow $row $forename $surname $email $phone
}

This function adds an address to the addressbook application. It begins by retrieving the row count, then it uses the custom chooseMenuItem function to invoke the Edit > Add menu option to pop up the Add dialog, and then it types in each piece of information into the appropriate line editor. Next, it clicks the dialog's OK button. Once the dialog has been accepted the row count is retrieved once more and we verify that it is now one more than before. We also check that every item of data was correctly entered into the table using a custom checkTableRow function.

One slightly tricky part at the end of the function is that the row we ask the checkTableRow function to verify must be computed with care. If there are no addresses (which is the initial case), the new address will be inserted at row 0 (the first row). But each subsequent address is inserted before the current row (and the current row is always the one that was inserted before). So the second address is also inserted at row 0, the third address at row 1, and so on. This is simply a behavioral quirk of our addressbook application that we must account for in our test.

FORENAME, SURNAME, EMAIL, PHONE = list(range(4))

    tableWidget = waitForObject(
            names.address_Book_Unnamed_File_QTableWidget)
    test.compare(forename, tableWidget.item(row, FORENAME).text(),
            "forename")
    test.compare(surname, tableWidget.item(row, SURNAME).text(), "surname")
    test.compare(email, tableWidget.item(row, EMAIL).text(), "email")
    test.compare(phone, tableWidget.item(row, PHONE).text(), "phone")
var FORENAME = 0;
var SURNAME = 1;
var EMAIL = 2;
var PHONE = 3;

{
    tableWidget = waitForObject(
        names.addressBookUnnamedFileQTableWidget);
    test.compare(forename, tableWidget.item(row, FORENAME).text(),
        "forename");
    test.compare(surname, tableWidget.item(row, SURNAME).text(),
        "surname");
    test.compare(email, tableWidget.item(row, EMAIL).text(), "email");
    test.compare(phone, tableWidget.item(row, PHONE).text(), "phone");
}
my $FORENAME = 0;
my $SURNAME  = 1;
my $EMAIL    = 2;
my $PHONE    = 3;

    my ( $row, $forename, $surname, $email, $phone ) = @_;
    my $tableWidget =
      waitForObject($Names::address_book_unnamed_file_qtablewidget);
    test::compare( $forename, $tableWidget->item( $row, $FORENAME )->text(),
        "forename" );
    test::compare( $surname, $tableWidget->item( $row, $SURNAME )->text(),
        "surname" );
    test::compare( $email, $tableWidget->item( $row, $EMAIL )->text(),
        "email" );
    test::compare( $phone, $tableWidget->item( $row, $PHONE )->text(),
        "phone" );
}
FORENAME = 0
SURNAME = 1
EMAIL = 2
PHONE = 3

  tableWidget = waitForObject(
  Names::Address_Book_Unnamed_File_QTableWidget)
  Test.compare(forename, tableWidget.item(row, FORENAME).text(),
  "forename")
  Test.compare(surname, tableWidget.item(row, SURNAME).text(), "surname")
  Test.compare(email, tableWidget.item(row, EMAIL).text(), "email")
  Test.compare(phone, tableWidget.item(row, PHONE).text(), "phone")
end
proc checkTableRow {row forename surname email phone} {
    set FORENAME 0
    set SURNAME 1
    set EMAIL 2
    set PHONE 3
    set tableWidget [waitForObject \
            $names::Address_Book_Unnamed_File_QTableWidget]
    set text [invoke [invoke $tableWidget item $row $FORENAME] text]
    test compare $forename $text "forename"
    set text [invoke [invoke $tableWidget item $row $SURNAME] text]
    test compare $surname $text "surname"
    set text [invoke [invoke $tableWidget item $row $EMAIL] text]
    test compare $email $text "email"
    set text [invoke [invoke $tableWidget item $row $PHONE] text]
    test compare $phone $text "phone"
}

This function compares each table cell that has just been edited with the text that was typed into it to verify that they are the same.

def removeAddress(email):
    tableWidget = waitForObject(
        names.address_Book_Unnamed_File_QTableWidget)
    oldRowCount = getRowCount()
    for row in range(oldRowCount):
        if tableWidget.item(row, EMAIL).text() == email:
            tableWidget.setCurrentCell(row, EMAIL)
            chooseMenuItem("Edit", "Remove...")
            clickButton(waitForObject(
                names.address_Book_Delete_Yes_QPushButton))
            test.log("Removed %s" % email)
            break
    newRowCount = getRowCount()
    test.verify(oldRowCount - 1 == newRowCount, "row count")
function removeAddress(email) {
    tableWidget = waitForObject(
        names.addressBookUnnamedFileQTableWidget)
    var oldRowCount = getRowCount();
    for (var row = 0; row < oldRowCount; ++row) {
        if (tableWidget.item(row, EMAIL).text() == email) {
            tableWidget.setCurrentCell(row, EMAIL);
            chooseMenuItem("Edit", "Remove...");
            clickButton(waitForObject(
                    names.addressBookDeleteYesQPushButton))
            test.log("Removed " + email);
            break;
        }
    }
    var newRowCount = getRowCount();
    test.verify(oldRowCount - 1 == newRowCount, "row count");
}
sub removeAddress {
    my $email = shift;
    my $tableWidget =
      waitForObject($Names::address_book_unnamed_file_qtablewidget);
    my $oldRowCount = getRowCount();
    for ( my $row = 0 ; $row < $oldRowCount ; ++$row ) {
        if ( $tableWidget->item( $row, $EMAIL )->text() eq $email ) {
            $tableWidget->setCurrentCell( $row, $EMAIL );
            chooseMenuItem( "Edit", "Remove..." );
            clickButton(
                waitForObject($Names::address_book_delete_yes_qpushbutton) );
            test::log("Removed $email");
            last;
        }
    }
    my $newRowCount = getRowCount();
    test::verify( $oldRowCount - 1 == $newRowCount, "row count" );
}
def removeAddress(email)
  tableWidget = waitForObject(
  Names::Address_Book_Unnamed_File_QTableWidget)
  oldRowCount = getRowCount()
  for row in 0...oldRowCount
    if tableWidget.item(row, EMAIL).text() == email
      tableWidget.setCurrentCell(row, EMAIL)
      chooseMenuItem("Edit", "Remove...")
      clickButton(waitForObject(
      Names::Address_Book_Delete_Yes_QPushButton))
      Test.log("Removed #{email}")
      break
    end
  end
  newRowCount = getRowCount()
  Test.verify(oldRowCount - 1 == newRowCount, "row count")
end
proc removeAddress {email} {
    set EMAIL 2
    set tableWidget [waitForObject \
        $names::Address_Book_Unnamed_File_QTableWidget]
    set oldRowCount [getRowCount]
    for {set row 0} {$row < $oldRowCount} {incr row} {
        set text [toString [invoke [invoke $tableWidget item $row $EMAIL] text]]
        if {[string equal $text $email]} {
            invoke $tableWidget setCurrentCell $row $EMAIL
            chooseMenuItem "Edit" "Remove..."
            invoke clickButton [waitForObject \
                $names::Address_Book_Delete_Yes_QPushButton]
            test log "Removed $email"
            break
        }
    }
    set newRowCount [getRowCount]
    test compare [expr {$oldRowCount - 1}] $newRowCount "row count"
}

To make it easier for testers who are populating the keyword data we have provided a removeAddress function that takes an email address to identify which row to delete. This is based on the reasonable assumption that every email address in the address book is unique.

The function begins by retrieving a reference to the AUT's table and also the current row count. It then iterates over every row until it finds one with a matching email address. Once it has a match it makes the corresponding cell the current one and invokes the AUT's Edit > Remove menu option to delete it. This menu option results in a Yes/No confirmation dialog popping up—the function clicks the dialog's Yes button. Once the deletion is done the loop is broken out of and we verify that the row count is one less than it was before.

def terminate():
    sendEvent("QCloseEvent", waitForObject(names.address_Book_MainWindow))
    clickButton(waitForObject(names.address_Book_No_QPushButton))
function terminate()
{
    sendEvent("QCloseEvent", waitForObject(names.addressBookMainWindow));
    clickButton(waitForObject(names.addressBookNoQPushButton));
}
sub terminate {
    sendEvent( "QCloseEvent", waitForObject($Names::address_book_mainwindow) );
    clickButton( waitForObject($Names::address_book_no_qpushbutton) );
}
def terminate
  sendEvent("QCloseEvent", waitForObject(Names::Address_Book_MainWindow))
  clickButton(waitForObject(Names::Address_Book_No_QPushButton))
end
proc terminate {} {
    sendEvent QCloseEvent [waitForObject $names::Address_Book_MainWindow]
    invoke clickButton [waitForObject $names::Address_Book_No_QPushButton]
}

To terminate the AUT we must first invoke the File > Quit menu option and then click No on the save changes dialog that pops up so that we cleanly exit without saving anything.

This completes the review of the AUT-specific functions. All the functions except for the getRowCount helper function are used in the keyword data. The only missing piece is the driver function that will take the keyword data and use it to call the AUT-specific functions: we will cover this in the next subsection.

How to Create a Generic Keyword Driver Function

The driver.py file (or driver.js, and so on), provides a single function, driver, that accepts a keywords data file as its sole argument and executes the commands specified in that data.

source(findFile("scripts", "actions.py"))

    test.log("Drive: '%s'" % datafile)
    for _, record in enumerate(testData.dataset(datafile)):
        command = testData.field(record, "Keyword") + "("
        comma = ""
        for i in range(1, 5):
            arg = testData.field(record, "Argument %d" % i)
            if arg:
                command += "%s%r" % (comma, arg)
                comma = ", "
            else:
                break
        command += ")"
        test.log("Execute: %s" % command)
        eval(command)
source(findFile("scripts", "actions.js"));

{
    test.log("Drive: '" + datafile + "'");
    var records = testData.dataset(datafile);
    for (var row = 0; row < records.length; ++row) {
        var command = testData.field(records[row], "Keyword") + "(";
        var comma = "";
        for (var i = 1; i <= 4; ++i) {
            var arg = testData.field(records[row], "Argument " + i);
            if (arg != "") {
                command += comma + "'" + arg + "'";
                comma = ", ";
            }
            else {
                break;
            }
        }
        command += ")";
        test.log("Execute: " + command);
        eval(command);
    }
}
source( findFile( "scripts", "actions.pl" ) );

    my $datafile = shift;
    test::log("Drive: '$datafile'");
    my @records = testData::dataset($datafile);
    for ( my $row = 0 ; $row < scalar(@records) ; ++$row ) {
        my $command = testData::field( $records[$row], "Keyword" ) . "(";
        my $comma = "";
        for ( my $i = 1 ; $i <= 4 ; ++$i ) {
            my $arg = testData::field( $records[$row], "Argument $i" );
            if ( $arg ne "" ) {
                $command .= "$comma\"$arg\"";
                $comma = ", ";
            }
            else {
                last;
            }
        }
        $command .= ");";
        test::log("Execute: $command");
        eval $command;
    }
}
def drive(datafile)
  require findFile("scripts", "actions.rb")
  Test.log("Drive: '#{datafile}'")
  TestData.dataset(datafile).each_with_index do
    |record, row|
    command = TestData.field(record, "Keyword") + "("
    comma = ""
    for i in 1...5
      arg = TestData.field(record, "Argument #{i}")
      if arg and arg != ""
        command += "#{comma}'#{arg}'"
        comma = ", "
      else
        break
      end
    end
    command += ")"
    Test.log("Execute: #{command}")
    eval command
  end
end
source [findFile "scripts" "actions.tcl"]

    test log "Drive: '$datafile'"
    set data [testData dataset $datafile]
    for {set row 0} {$row < [llength $data]} {incr row} {
        set command [testData field [lindex $data $row] "Keyword"]
        for {set i 1} {$i <= 4} {incr i} {
            set arg [testData field [lindex $data $row] "Argument $i"]
            if {$arg != ""} {
                set command "${command} \"${arg}\""
            } else {
                break
            }
        }
        test log "Execute: $command"
        eval $command
    }
}

The first thing that must be done is to access the AUT-specific actions by importing the actions.py file (or actions.js and so on).

The drive function iterates over every row in the test data (Squish's Dataset testData.dataset(filename) function automatically skips over the first row that has the field names). For each row the function retrieves the keyword (i.e., the name of the AUT-specific function to execute), and then the arguments. In this case we have limited the keyword data to have up to four arguments but it is easy to allow more.

For each keyword data record we create a command string consisting of the AUT-specific function to call and any arguments that have been given. Once the command has been prepared we log what is about to be executed and then evaluate (i.e., execute) the command.

This completes the under-the-hood functionality required to support keyword-driven testing in Squish. None of the work needed is particularly difficult. The driver function need be written only once since it can be used with any AUTs no matter what GUI toolkits they use (providing only that the AUT-specific functions are in a script called actions.py or actions.js and so on, as appropriate for the scripting language). The AUT-specific functionality need be written only once per AUT, although some functions might be reusable across AUTs that use the same GUI toolkit.

© 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.