如何进行关键字驱动测试

关键字驱动测试(也称为“表格驱动测试”和“操作词测试”)是一种测试方法,其中测试完全由数据驱动。关键字驱动测试与数据驱动测试的不同之处在于,在后一种中,我们只是读取数据项,例如,填充GUI表格,但在前一种中,数据项不仅仅是数据,而是AUT特定函数及其参数的名称,这些参数随后作为测试运行时执行。

关键字驱动测试的巨大优势是,测试可以纯粹以数据表的形式创建,涉及高级AUT操作,例如“添加项目”或“删除项目”,测试人员无需了解底层更技术化的方面即可与之相关联。

创建关键字驱动测试涉及两个阶段。首先是创建一些AUT特定的测试脚本函数,以解释数据,以及一个通用的“驱动器”函数,该函数从数据源读取测试数据,并根据数据执行AUT特定的测试脚本函数。其次是创建一个或多个测试案例和对应的数据表,这些数据表用于驱动测试。

在本节中,我们将首先了解测试人员如何创建测试案例及其相应的测试数据,以及由此产生的结果;然后我们将了解幕后必须进行的单一工作以使其发挥作用。

本节中展示的所有示例均位于Squish的示例目录中(Python版本为SQUISHDIR/examples/qt/addressbook/suite_keyword_py,JavaScript版本为SQUISHDIR/examples/qt/addressbook/suite_keyword_js,等等)。测试案例本身被称为tst_keyword_driven

尽管我们使用了基于Qt的AUT,但底层的GUI工具包并不重要——使用本节中展示的想法,您可以为Squish支持的所有工具包创建关键字驱动测试。

如何创建关键字驱动测试

关键字驱动测试所需的测试案例总是相同的——而且非常简单

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"
}

首先,测试程序加载通用“驱动器”功能,然后它在一个测试数据文件上执行drive()函数。测试数据文件指定了应执行的确切操作,从启动AUT开始,以终止AUT结束。

以下是一个典型(但相当小)的数据文件可能的样子

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

(我们使用箭头符号→表示制表符分隔符。)第一行包含字段名称,其他行包含要执行的操作。在每个操作行中,第一列包含要执行的高级AUT特定函数的名称,其他列包含该函数可能需要的任何参数。

在此,terminate函数不需要参数,而verifyRowCountremoveAddress函数都要求一个参数(前者为要验证的行数,后者为用于标识要删除的行的电子邮件地址)。类似地,chooseMenuItem函数始终需要两个参数(菜单的名称和菜单项的名称),而addAddress函数需要四个参数(名、姓、电子邮件、电话)。

以下是典型测试运行产生的结果(日期和大部分项目已省略)

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', '[email protected]',
'555 123 4567')
Pass Verified
Pass Comparison
Pass Comparison
Pass Comparison
Pass Comparison
...
Log Execute: verifyRowCount('3')
Pass Verified
Log Execute: removeAddress('[email protected]')
Log Removed green.pike@froglogic.com
Pass Verified
...
Log Execute: verifyRowCount('0')
Pass Verified
Log Execute: terminate()

显而易见,我们可以轻松地添加任意数量的关键字动作——这完全独立于测试用例脚本,脚本将简单地执行数据文件中指定的任何测试。当然,如果需要额外的动作——如一个editAddress函数——也没有什么理由不能将其添加为AUT特定的函数,一旦添加,就可以在数据文件中使用它,以及其他所有内容。

因此,对于想要进行关键字驱动的测试的测试人员来说,工作非常直接。然而,要实现这种简单性需要编写AUT特定的函数,这些函数是测试数据需要执行的,以及通用驱动函数。这些将在下一两个小节中介绍。

如何创建针对关键字驱动的AUT特定支持

测试数据中指定的每个关键字动作都必须有一个相应的函数来接受给定的参数并执行给定的操作。由于每个动作都是AUT特定的,所以没有通用的解决方案。不过,为了完整性,我们将展示上述测试数据中所使用的所有动作的实现,只有一个例外。这个例外是ApplicationContext startApplication(autName)函数,它已经内置到Squish中,所以我们不必自己实现它。

以下函数全部来自action.py(或action.js等等)脚本。

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]
}

该函数激活了指定的菜单,然后激活指定的菜单项。我们使用 了从对象映射获取的符号名称。

当然,对象映射最初是空的,所以我们做的第一件事——在创建任何函数之前——是记录和回播一个假测试。在这个测试中,我们做了在关键字驱动测试中计划做的所有事情(但没有重复)。因此我们创建了一个新文件,添加了一个地址,删除了一个地址,然后退出。这为我们的大多数所需名称填充了对象映射。

菜单栏和菜单选项有多个符号名称来引用它们,因为随着AUT状态的改变,其窗口标题也会改变,Squish通过创建多个符号名称,每个符号名称都有一个不同的window属性值来跟踪这一点。这个属性值取决于窗口的标题,而窗口的标题本身也在变化。我们需要能够在不受AUT状态(尤其是窗口标题)影响的情况下访问菜单栏和菜单项。

为了解决这个问题,我们使用对象映射视图来编辑对象映射。首先,我们从":Address Book_QMenuBar"项中删除了window属性,这样就可以在任何窗口标题下找到菜单栏。然后,出于同样的原因,我们从":Address Book.File_QMenu"项中删除了window属性。然后我们复制了":Address Book.File_QMenu"符号名称并将其粘贴;我们改变了粘贴版本的":Address Book.Edit_QMenu",并将标题属性值从"File"改为"Edit"。然后我们删除了任何符号名称中包含"Unnamed"的菜单项。

编辑对象映射后,这个用于调用菜单及其某个菜单选项的函数可以完美地工作——不受AUT窗口标题的影响。

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"
}

该函数用于验证AUT的行数是否如我们所期望的那样。

所有的关键字数据都存储和检索为字符串。我们当然可以使用两列来存储数据项(如type, value),或其他类型指定方案(如type=value)。但这会使得测试人员应对不同类型的问题变得复杂。所以我们接受所有内容都以字符串的形式,当我们需要其他类型,如这里,我们在AUT特定的函数中执行转换。(除了Perl,在那里我们只是强制进行字符串比较。)

对于Python,由于Squish导入了与内置不同的Python特定int()函数,转换变得稍微复杂一些。为了解决这个问题,我们导入了Python的__builtin__模块(Python 2)或builtins模块(Python 3),然后访问Python自身int()转换函数,将rows字符串转换为数字(见Python笔记。)

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]
}

此辅助函数获取底层工具包中的表格引用(在本例中为一个QTableWidget),然后返回其rowCount属性值。表格的象征性名称是在虚拟测试运行后填写到对象映射中时复制的。

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
}

此函数向地址簿应用程序添加一个地址。它首先检索行数,然后使用自定义的chooseMenuItem函数调用“编辑”>“添加”菜单选项以弹出“添加”对话框,并将每项信息输入到相应的行编辑器中。接下来,它点击对话框中的“确定”按钮。对话框被接受后,再次检索行数,并验证它与之前相比多了1。我们还使用自定义的checkTableRow函数检查每个项目是否已正确输入到表中。

函数末尾有一个稍微复杂的部分是,我们需要谨慎地计算出需要通过checkTableRow函数验证的行。如果没有地址(这是初始情况),新地址将插入到第0行(第一行)。但是每个后续地址是在当前行之前插入的(当前行始终是插入之前的行)。因此,第二个地址也插入到第0行,第三个地址插入到第1行,依此类推。这是我们的地址簿应用程序的一个行为怪癖,我们必须在我们的测试中考虑到这一点。

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"
}

此函数将刚刚编辑过的每个表格单元格与输入到其中的文本进行比较,以验证它们是否相同。

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"
}

为了使测试人员更容易填充关键字数据,我们提供了一个removeAddress函数,它接受一个电子邮件地址来标识要删除的行。这基于一个合理的假设,即地址簿中的每个电子邮件地址都是唯一的。

函数首先检索端到端测试应用的表格引用和当前行数。然后,它遍历每一行,直到找到一个与电子邮件地址匹配的行。一旦匹配成功,它将相应的单元格设置为当前单元格,并使用端到端测试应用的“编辑”>“删除”菜单选项来删除它。此菜单选项会导致弹出Yes/No确认对话框——函数点击对话框的“是”按钮。删除完成后,循环跳出,并验证行数现在比之前少1。

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]
}

要终止端到端测试应用,我们必须首先调用“文件”>“退出”菜单选项,然后在弹出的“保存更改”对话框中点击“否”,以便我们干净退出且不保存任何内容。

这完成了对端到端测试应用特定函数的评审。除了辅助函数getRowCount之外,所有函数都用于关键字数据。唯一缺少的是驱动函数,该函数将使用关键字数据来调用端到端测试应用特定函数:我们将在下一部分内容中介绍这一点。

如何创建通用关键字驱动函数

driver.py文件(或driver.js等),提供了一个名为driver的单个函数,该函数接受一个关键字数据文件作为其唯一参数并执行该数据中指定的命令。

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
    }
}

首先必须做的是通过导入 actions.py 文件(或 actions.js 以及类似文件)来访问AUT特定的操作。

drive 函数会遍历测试数据中的每一行(Squish的 Dataset testData.dataset(filename) 函数会自动跳过含有字段名的第一行)。对于每一行,函数都会检索关键词(即要执行的AUT特定函数的名称)以及参数。在这种情况下,我们限制了关键词数据的参数数量最多为四个,但很容易扩展到更多。

对于每个关键词数据记录,我们创建一个命令字符串,其中包含要调用的AUT特定函数和任何给出的参数。一旦准备就绪,我们记录即将执行的内容,然后评估(即执行)该命令。

这完成了Squish中支持关键字驱动测试所需的后台功能。所需的工作并不特别困难。驱动函数只需要编写一次,因为它可以用在任何AUTs上,无论它们使用什么GUI工具包(只要AUT特定的函数位于名为 actions.pyactions.js 的脚本中,具体取决于脚本语言)。AUT特定的功能只需要为每个AUT编写一次,尽管一些函数可能在使用相同GUI工具包的AUTs之间可重用。

©2024 Qt公司有限公司。此处包含的文档贡献的版权归其各自所有者所有。
此处提供的文档根据自由软件基金会发布的版本1.3的 GNU自由文档许可证 的条款进行许可。
Qt及其相关商标是芬兰以及全世界其他国家的Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。