如何测试 Qt 应用程序
Squish for Qt 允许查找和查询对象,调用方法,访问属性和枚举。Squish 自动识别 QObject
和 QWidget
属性和槽。因此,您很少需要构建自定义包装器,因为您可以使用 Q_PROPERTY
宏来公开自定义对象属性,并通过将它们转换为槽来公开自定义对象方法。自 Qt 4.6 以来,这甚至适用于自动识别 QGraphicsWidget
和 QGraphicsObject
类及其派生的自定义子类的属性和槽。
- 如何访问 Qt 对象
- 如何在 Qt 对象上调用函数
- 如何访问 Qt 枚举
- 如何使用 Qt 便利 API
- 如何使用 Qt 信号处理器
- 如何测试 Qt 小部件
- 如何在 Qt 应用程序中测试非 Qt 小部件
- 如何对 Qt 进行自动压力测试
- 如何测试国际化 Qt 自动化测试
此外,Qt 便利 API 提供了执行常用 GUI 操作的功能,例如单击按钮或选择菜单项。
如何测试 Qt 小部件 包含使用脚本 Qt API 访问和测试复杂 Qt 应用程序的示例。
如何访问 Qt 对象
如 如何识别和访问对象 中所述,可以调用 Object waitForObject(objectOrName)(或用于隐藏对象的 Object findObject(objectName)),以获取具有特定真实或符号名称的对象的引用。一旦获得此类引用,就可以使用它来与对象交互,访问对象的属性或调用对象的方法。
以下是一些我们访问 QRadioButton 的示例。如果它没有被选中,我们点击它以选中它,这样最后它应该被选中,无论它的初始状态如何。
cashRadioButton = waitForObject(names.make_Payment_Cash_QRadioButton) if not cashRadioButton.checked: clickButton(cashRadioButton) test.verify(cashRadioButton.checked)
var cashRadioButton = waitForObject(names.makePaymentCashQRadioButton); if (!cashRadioButton.checked) { clickButton(cashRadioButton); } test.verify(cashRadioButton.checked);
my $cashRadioButton = waitForObject($cashRadioButtonName); if (!$cashRadioButton->checked) { clickButton($cashRadioButton); } test::compare($cashRadioButton->checked, 1);
cashRadioButton = waitForObject(Names::Make_Payment_Cash_QRadioButton) if not cashRadioButton.checked clickButton(cashRadioButton) end Test.verify(cashRadioButton.checked)
set cashRadioButton [waitForObject $names::Make_Payment_Cash_QRadioButton] if {![property get $cashRadioButton checked]} { invoke clickButton $cashRadioButton } test verify [property get $cashRadioButton checked]
在下面的示例中,我们获取属性值,设置属性(通过单击小部件间接地),然后再次获取属性值,以便我们可以测试它是否具有正确的值。
以下是另一个示例,这次是一个设置并获取 QLineEdit 的 text
属性并打印属性值到 Squish 测试日志的示例(即到 Test Results view)。与前面的示例相比,它使用了 基于文本的对象图。
lineedit = waitForObject(":Forename:_LineEdit") lineedit.text = "A new text" text = lineedit.text test.log(str(text))
var lineedit = waitForObject(":Forename:_LineEdit"); lineedit.text = "A new text"; var text = lineedit.text; test.log(String(text));
my $lineedit = waitForObject(":Forename:_LineEdit"); $lineedit->text = "A new text"; my $text = $lineedit->text; test::log("$text");
lineedit = waitForObject(":Forename:_LineEdit") lineedit.text = "A new text" text = lineedit.text Test.log(String(text))
set lineedit [waitForObject ":Forename:_LineEdit"] property set $lineedit text "A new text" set text [property get $lineedit.text] test log [toString $text]
转换 QString 到本地字符串
在上面的示例中,从 QLineEdit::text 查询的文本并不是直接传递给 test.log(message) 函数(或传递给本机打印函数,如 print
或 puts
)。该属性的类型是 QString,而打印字符串的脚本函数需要本机(针对脚本语言)字符串,例如 Python 中的 str
,或 JavaScript 或 Ruby 中的 String
。在示例中明确执行了转换,尽管在 Perl 的情况下我们间接地使用了字符串插值,而在 Tcl 的情况下我们使用了 Squish 内部辅助函数,toString()。
在逆向转换方面(即将本地字符串传递给期望 QString 的 Qt API 函数),Squish 会自动进行处理,因此在这种情况下不需要显式转换。
如何在 Qt 对象上调用函数
使用 Squish,可以调用任何 Qt 对象上的任何公共函数。此外,还可以调用 Qt 提供的静态函数。
下面的示例中,我们使用QButton::setText函数更改上一节中查询的按钮的文本。
button = waitForObject(":Address Book - Add.OK_QPushButton") button.setText("Changed Button Text")
var button = waitForObject(":Address Book - Add.OK_QPushButton"); button.setText("Changed Button Text");
my $button = waitForObject(":Address Book - Add.OK_QPushButton"); $button->setText("Changed Button Text");
button = waitForObject(":Address Book - Add.OK_QPushButton") button.setText("Changed Button Text")
set button [waitForObject ":Address Book - Add.OK_QPushButton"] invoke $button setText "Changed Button Text"
同样,也可以调用静态 Qt 函数。例如,我们将使用静态的QApplication::activeModalWidget函数查询当前活动窗口(例如,对话框)。如果返回有效对象,我们将对象的名称(如果没有设置则显示“无名称”)打印到测试日志(即测试结果视图)。要检查对象是否有效(即非空),可以使用 Squish 的bool isNull(object)函数。要获取对象的名称,我们访问其objectName
属性。
widget = QApplication.activeModalWidget() if not isNull(widget): test.log(widget.objectName or "unnamed")
var widget = QApplication.activeModalWidget(); if (!isNull(widget)) { var name = widget.objectName; test.log(name.isEmpty() ? "unnamed" : name); }
my $widget = QApplication::activeModalWidget(); if (!isNull($widget)) { test::log($widget->objectName() || "unnamed"); }
widget = QApplication.activeModalWidget() if !isNull(widget) name = widget.objectName Test.log(name != "" ? name : "unnamed") end
set widget [invoke QApplication activeModalWidget] if {![isNull $widget]} { set name [property get $widget objectName] if {[invoke $name isEmpty]} { set name "unnamed" } test log stdout "$name\n" }
如何访问 Qt 枚举
在 C++ 中,可以声明枚举,即代表数字的名称,以使数字的意义和用途清晰。例如,在写label->setAlignment(1);
时,程序员可以写label->setAlignment(Qt::AlignLeft);
,这更易于理解。“枚举”一词通常缩写为“enum”。在本手册中,我们使用这两种形式。
Qt 定义了许多枚举,许多 Qt 函数和方法都接受枚举作为参数。与使用枚举使 C++ 程序员代码更清晰一样,它也可以使测试代码更清晰,因此 Squish 使可能在测试脚本中使用枚举。以下是设置标签对齐方式的示例
label = waitForObject(":Address Book - Add.Forename:_QLabel") label.setAlignment(Qt.AlignLeft)
var label = waitForObject(":Address Book - Add.Forename:_QLabel"); label.setAlignment(Qt.AlignLeft);
my $label = waitForObject(":Address Book - Add.Forename:_QLabel"); $label->setAlignment(Qt::AlignLeft);
label = waitForObject(":Address Book - Add.Forename:_QLabel") label.setAlignment(Qt::ALIGN_LEFT)
set label [waitForObject ":Address Book - Add.Forename:_QLabel"] invoke $label setAlignment [enum Qt AlignLeft]
如何使用 Qt 便利 API
本节描述 Squish 在标准 Qt API 之上提供的脚本 API,以便轻松执行常见的用户操作,例如单击按钮或激活菜单选项。此 API 的完整列表可以在工具参考中的Qt 便利 API部分找到。
以下是 API 使用的示例。第一行显示如何单击按钮,第二行显示如何双击项(例如,列表、表格或树中的项—尽管在这里我们单击表格中的项),最后一个示例显示如何激活菜单选项(在这种情况下,文件 > 打开)。
clickButton(":Address Book - Add.OK_QPushButton") doubleClickItem(":CSV Table - before.csv.File_QTableWidget", "10/0", 22, 20, 0, Qt.LeftButton) activateItem(waitForObjectItem(":Address Book_QMenuBar", "File")) activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."))
clickButton(":Address Book - Add.OK_QPushButton"); doubleClickItem(":CSV Table - before.csv.File_QTableWidget", "10/0", 22, 20, 0, Qt.LeftButton); activateItem(waitForObjectItem(":Address Book_QMenuBar", "File")); activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."));
clickButton(":Address Book - Add.OK_QPushButton"); doubleClickItem(":CSV Table - before.csv.File_QTableWidget", "10/0", 22, 20, 0, Qt.LeftButton); activateItem(waitForObjectItem(":Address Book_QMenuBar", "File")); activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."));
clickButton(":Address Book - Add.OK_QPushButton") doubleClickItem(":CSV Table - before.csv.File_QTableWidget", "10/0", 22, 20, 0, Qt::LEFT_BUTTON) activateItem(waitForObjectItem(":Address Book_QMenuBar", "File")) activateItem(waitForObjectItem(":Address Book.File_QMenu", "Open..."))
invoke clickButton ":Address Book - Add.OK_QPushButton" invoke doubleClickItem ":CSV Table - before.csv.File_QTableWidget" \ "10/0" 22 20 0 [enum Qt LeftButton] invoke activateItem [waitForObjectItem ":Address Book_QMenuBar" "File"] invoke activateItem [waitForObjectItem ":Address Book.File_QMenu" "Open..."]
有关如何测试各种 Qt 组件的示例,请参阅如何测试 Qt 组件部分。
如何使用 Qt 信号处理器
注意:本节仅适用于 Squish for Qt。
要跟踪用户界面(或任何 AUT QObject
)中由小部件发出的 Qt 信号,请使用qtInstallSignalHandler(objectOrName, signalSignature, handlerFunctionName)函数。
qtInstallSignalHandler(objectOrName, signalSignature, handlerFunctionName)函数应在 AUT 启动后调用,并传递现有 AUT 对象的名称或引用、信号签名和处理函数的名称(作为字符串)。如果需要,可以多次调用该函数以注册多个对象/信号/处理程序组合。以下是一个非常简单的示例,展示如何执行此操作
def tableItemChangedHandler(obj, item): test.log('itemChanged emitted by object "%s" on item "%s"' % ( objectMap.symbolicName(obj), item.text())) def main(): startApplication("addressbook") # ... various actions ... now the table widget exists installSignalHandler( ":Address Book - MyAddresses.adr.File_QTableWidget", "itemChanged(QTableWidgetItem*)", "tableItemChangedHandler") # ... the rest of the test ...
function tableItemChangedHandler(obj, item) { test.log('itemChanged emitted by object "' + objectMap.symbolicName(obj) + '" on item "' + item.text() + '"'); } function main() { startApplication("addressbook"); // ... various actions ... now the table widget exists installSignalHandler( ":Address Book - MyAddresses.adr.File_QTableWidget", "itemChanged(QTableWidgetItem*)", "tableItemChangedHandler"); // ... the rest of the test ... }
sub tableItemChangedHandler { my($obj, $item) = @_; test::log("itemChanged emitted by object \"" . objectMap::symbolicName($obj) . "\" on item \"" . $item->text() . "\""); } sub main { startApplication("addressbook"); # ... various actions ... now the table widget exists installSignalHandler( ":Address Book - MyAddresses.adr.File_QTableWidget", "itemChanged(QTableWidgetItem*)", "main::tableItemChangedHandler"); # ... the rest of the test ... }
def tableItemChangedHandler(obj, item) name = objectMap.symbolicName(obj) text = item.text() Test.log("itemChanged emitted by object '#{name}' on item '#{text}'") end def main startApplication("addressbook") # ... various actions ... now the table widget exists installSignalHandler( ":Address Book - MyAddresses.adr.File_QTableWidget", "itemChanged(QTableWidgetItem*)", "tableItemChangedHandler") # ... the rest of the test ... end
proc tableItemChangedHandler {obj item} { set name [objectMap symbolicName $obj] set text [toString [invoke $item text]] test log "itemChanged was emitted by object \"$name\" on item \"$text\"" } proc main {} { startApplication "addressbook" # ... various actions ... now the table widget exists invoke installSignalHandler \ ":Address Book - MyAddresses.adr.File_QTableWidget" \ "itemChanged(QTableWidgetItem*)" "tableItemChangedHandler" # ... the rest of the test ... }
每当《QTableWidget》中的任何项目发生更改时,《tableItemChangedHandler》函数将使用触发信号的表小部件的引用被调用。在这里,我们简单地使用《String objectMap.symbolicName(object)》函数,即使用objectMap.symbolicName(object),记录发送信号的实体的符号名称和被更改的《QTableWidgetItem》文本。因此,每当发出信号时(即,每当表格项发生更改时),我们都会得到类似这样的日志输出:
itemChanged emitted by object ":Address Book - MyAddresses.adr.File_QTableWidget" on item "Doe"
我们将输出换行以使其更容易阅读。
处理函数传递的第一个参数始终是触发信号的实体的引用。如果信号有任何参数,则这些参数也按照对象引用顺序传递给处理函数。因此,在上面的例子中,itemChanged(QTableWidgetItem*)
信号有一个参数,因此处理程序获取两个参数——触发对象和信号的表格项目。
我们可以注册任何数量的处理函数。以下是一些额外的处理程序示例
def fileMenuHandler(obj, action): test.log('triggered emitted by object "%s" for action "%s"' % ( objectMap.symbolicName(obj), action.text)) def modelIndexClickedHandler(obj, index): test.log('clicked emitted by object "%s" on index "%s"' % ( objectMap.symbolicName(obj), index.text)) def cellClickedHandler(obj, row, column): test.log('clicked emitted by object "%s" on cell (%d, %d)' % ( objectMap.symbolicName(obj), row, column))
function fileMenuHandler(obj, action) { test.log('triggered emitted by object "' + objectMap.symbolicName(obj) + '" for action "' + action.text + '"'); } function modelIndexClickedHandler(obj, index) { test.log('clicked emitted by object "' + objectMap.symbolicName(obj) + '" on index "' + index.text + '"'); } function cellClickedHandler(obj, row, column) { test.log('clicked emitted by object "' + objectMap.symbolicName(obj) + 'on cell (' + row + ', ' + column + ')'); }
sub fileMenuHandler { my($obj, $action) = @_; test::log("triggered emitted by object \"" . objectMap::symbolicName($obj) . "\" for action \"" . $action->text . "\""); } sub modelIndexClickedHandler { my($obj, $index) = @_; test::log("clicked emitted by object \"" . objectMap::symbolicName($obj) . "\" on index \"" . $index->text . "\""); } sub cellClickedHandler { my($obj, $row, $column) = @_; test::log("clicked emitted by object \"" . objectMap::symbolicName($obj) . "\" on cell ($row, $column)"); }
def fileMenuHandler(obj, action) name = objectMap.symbolicName(obj) text = action.text Test.log("triggered emitted by object '#{name}' for action '#{text}'") end def modelIndexClickedHandler(obj, index) name = objectMap.symbolicName(obj) text = index.text Test.log("triggered emitted by object '#{name}' on index '#{text}'") end def cellClickedHandler(obj, row, column) name = objectMap.symbolicName(obj) Test.log("clicked emitted by object '#{name}' on cell (#{row}, #{column})") end
proc fileMenuHandler {obj action} { set name [objectMap symbolicName $obj] set text [toString [property get $action text]] test log "triggered emitted by object \"$name\" for action \"$text\"" } proc modelIndexClickedHandler {obj index} { set name [objectMap symbolicName $obj] set text [toString [property get $index text]] test log "triggered emitted by object \"$name\" on index \"$text\"" } proc cellClickedHandler {obj row column} { set name [objectMap symbolicName $obj] set row [toString $row] set column [toString $column] test log "clicked emitted by object \"$name\" on cell ($row, $column)" }
以下是安装处理程序的代码
installSignalHandler(":Address Book.File_QMenu", "triggered(QAction*)", "fileMenuHandler") table = waitForObject( ":Address Book - MyAddresses.adr.File_QTableWidget") installSignalHandler(table, "clicked(QModelIndex)", "modelIndexClickedHandler") installSignalHandler(table, "cellClicked(int, int)", "cellClickedHandler")
installSignalHandler(":Address Book.File_QMenu", "triggered(QAction*)", "fileMenuHandler"); var table = waitForObject( ":Address Book - MyAddresses.adr.File_QTableWidget"); installSignalHandler(table, "clicked(QModelIndex)", "modelIndexClickedHandler"); installSignalHandler(table, "cellClicked(int, int)", "cellClickedHandler");
installSignalHandler(":Address Book.File_QMenu", "triggered(QAction*)", "main::fileMenuHandler"); my $table = waitForObject( ":Address Book - MyAddresses.adr.File_QTableWidget"); installSignalHandler($table, "clicked(QModelIndex)", "main::modelIndexClickedHandler"); installSignalHandler($table, "cellClicked(int, int)", "main::cellClickedHandler");
installSignalHandler(":Address Book.File_QMenu", "triggered(QAction*)", "fileMenuHandler") table = waitForObject( ":Address Book - MyAddresses.adr.File_QTableWidget") installSignalHandler(table, "clicked(QModelIndex)", "modelIndexClickedHandler") installSignalHandler(table, "cellClicked(int, int)", "cellClickedHandler")
invoke installSignalHandler \ ":Address Book.File_QMenu" "triggered(QAction*)" "fileMenuHandler" set table [waitForObject ":Address Book - MyAddresses.adr.File_QTableWidget"] invoke installSignalHandler $table "clicked(QModelIndex)" \ "modelIndexClickedHandler" invoke installSignalHandler $table "cellClicked(int, int)" \ "cellClickedHandler"
请记住,只能为已存在的对象安装处理函数。此处所示的所有示例代码均来自《examples/qt/addressbook/suite_*/tst_signal_handler》,在那里您可以在上下文中查看它。
如何测试 Qt 小部件
在本节中,我们将了解 Squish API 如何使检查单个小部件的值和状态变得简单,以便我们可以测试应用程序的业务规则。
正如我们在教程中看到的,我们可以使用 Squish 的录制功能创建测试。然而,修改此类测试或从代码中从头创建测试通常很有用,尤其是当我们想要测试涉及多个小部件的业务规则时。
通常无需测试小部件的标准行为。例如,如果未选中的两位复选框在被点击后未选中,则为工具包中的错误,而不是我们的代码中的错误。如果发生此类情况,我们可能需要编写一个助手(并为它编写测试),但通常我们不会编写测试只是为了检查我们的底层 API 是否按文档所述工作(除非我们是 API 的开发者)。另一方面,我们想要测试的是,我们的应用程序是否提供了我们 intend 建立到其中的业务规则。一些测试涉及单独的小部件——例如,测试组合框是否包含合适的项。其他测试涉及小部件之间的依赖关系和交互。例如,如果我们有一组“支付方式”单选按钮,我们将想要测试,如果勾选《现金》单选按钮,相关的《支票》和《信用卡》小部件将被隐藏。
无论我们是在测试单个小部件还是在测试小部件之间的依赖关系和交互,我们首先必须能够识别我们想要测试的小部件。一旦已识别,我们可以验证它们是否具有我们期望的值并处于我们期望的状态。识别小部件的一种方法是通过涉及其使用情况的测试记录,并查看 Squish 为其使用什么名称。但最简单的方法是使用进程工具识别小部件,以便我们在测试代码中使用它。更多信息,请参阅《如何使用 Spy》和《waitForObject(objectOrName)》。
本节说明了如何使用 Squish 支持的所有脚本语言访问各种 Qt 小部件以及使用这些小部件进行常见操作(例如获取和设置它们的属性)。
完成本节后,您应该能够访问 Qt 小部件,从那些 Qt 小部件中收集数据,并对预期值进行测试。本章所述原则适用于所有 Qt 小部件,因此即使您需要测试此处未明确提及的小部件,也应该没有问题。
要在代码中测试和验证小部件及其属性或内容,首先我们需要在测试脚本中获得对小部件的访问权限。为了获取小部件的引用,使用的是 Object waitForObject(objectOrName) 函数。该函数根据给定的名称找到小部件,并返回对其的引用。为此,我们需要知道我们想要测试的小部件的名称,并且我们可以使用间谍工具(见 如何使用间谍工具)并将对象添加到 对象映射(这样 Squish 将记住它)中,然后将对象的名称(最好是它的符号名称)复制到剪贴板,以便粘贴到我们的测试中。如果我们需要收集大量小部件的名称,记录一个模拟测试可能会更快更容易,在模拟测试中我们确保手动编写的测试脚本中访问我们要验证的所有小部件。这将导致 Squish 将所有相关名称添加到 对象映射,然后我们可以将其复制并粘贴到我们的代码中。
如何测试小部件状态和属性
每个 Qt 小部件都有一组与它关联的属性和状态,我们可以使用 Squish 在测试脚本中对其进行查询,以执行检查。这些属性可以是诸如,焦点(小部件是否有键盘焦点)、启用状态(此小部件是否启用)、可见性(小部件是否可见)、高度(小部件的高度)、宽度(小部件的宽度)等。所有这些属性都在 Qt 项目网站 上有文档记载。只需选择您正在运行的 Qt 版本(例如:Qt 4.8),并搜索您想要验证属性的对象的 Qt 类。
例如,假设我们在应用程序中有一个按钮,您使用了间谍工具发现该小部件的 Qt 类名为 QPushButton。在网站上的 所有类 部分,搜索 QPushButton
并点击它。您会看到这个小部件只有几个属性,然而,它还继承了 QAbstractButton 类的额外属性,以及从 QWidget 类和 QObject 类继承的许多属性。通过访问这些基类中的每一个,您将看到您可以在测试脚本中用 Squish 查询的所有属性。在以下章节中,我们将看到许多访问和测试小部件属性示例。
阅读工具包的文档对于了解小部件具有哪些属性和了解它们非常有用。然而,如果使用 Squish 间谍,我们可以看到 AUT 的所有对象以及所选对象的所有属性及其值。由于大多数属性都具有合理的名称,这通常足以看到特定小部件拥有哪些属性以及我们想要验证哪些属性。有关详细信息,请参阅 间谍视角 和它交叉引用的视图。
如何测试具有状态和单值的小部件
在本节中,我们将介绍如何测试examples/qt/paymentform
示例程序。此程序使用了众多基本的Qt控件,包括QCheckBox、QComboBox、QDateEdit、QLineEdit、QPushButton、QRadioButton和QSpinBox。作为我们对示例的覆盖范围的一部分,我们将展示如何检查各个小部件的值和状态。我们还将演示如何测试表单的业务规则。
“按支票支付”模式下的paymentform
示例。
当需要支付账单时,无论是现场支付,还是对于信用卡,通过电话支付时,会调用paymentform
。表单上的Pay按钮只有在正确填写并输入有效的值时才应启用。我们必须测试的业务规则如下:
- 在“现金”模式下,即当现金 QRadioButton被选中时
- 不相关的控件(例如,账户名称、账户号码)不得可见。(由于表单使用QStackedWidget,我们只需检查现金控件是否可见,而支票和信用卡控件是否被隐藏。)
- 最低付款额为一美元,最高付款额为2000美元或应付金额中的较小值。
- 在“支票”模式下,即当支票 QRadioButton被选中时
- 不相关的控件(例如,出票日期、到期日期)不得可见。(实际上,我们只需检查支票控件是否可见,而现金和信用卡控件是否被隐藏。)
- 最低付款额为10美元,最高付款额为250美元或应付金额中的较小值。
- 支票日期不得早于30天前,也不得晚于明天。
- 银行名称、银行号、账户名称和账户号码的行编辑必须都不为空。
- 支票签收复选框必须被选中。
- 在“信用卡”模式下,即当信用卡 QRadioButton被选中时
- 不相关的控件(例如,支票日期、支票签收)不得可见。(实际上,我们只需检查信用卡控件是否可见,而支票和信用卡控件是否被隐藏。)
- 最低付款额为10美元或应付金额的5%,以较大者为准,最高付款额为5000美元或应付金额中的较小值。
- 对于非维萨卡,出票日期不得早于三年前。
- 到期日期必须至少比今天晚一个月。
- 账户名称和账户号码的行编辑必须都不为空。
我们将编写三个测试,每个模式一个,以便于检查QStackedWidget中的小部件,我们为它们明确地指定了对象名称(使用QObject的setObjectName方法)——“CashWidget”、“CheckWidget”和“CardWidget”。同样,我们也将名称“AmountDueLabel”赋予显示应付金额的QLabel控件。
支付表单的源代码在SQUISHDIR/examples/qt/paymentform
目录中,测试套件在以下子目录中——例如,测试的Python版本在SQUISHDIR/examples/qt/paymentform/suite_py
目录中,测试的JavaScript版本在SQUISHDIR/examples/qt/paymentform/suite_js
目录中,以此类推。
我们首先将回顾测试脚本,用于测试表单的“现金”模式。所有代码都包含在一个庞大的main
函数中。不用担心代码看起来很长。在我们查看下一个测试脚本时,我们将看到如何将代码分解成可管理的部分。我们将按部分展示函数,并在每个部分之后解释。
def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/paymentform/paymentform"') # Make sure the Cash radio button is checked so we start in the mode # we want to test cashRadioButton = waitForObject(names.make_Payment_Cash_QRadioButton) if not cashRadioButton.checked: clickButton(cashRadioButton) test.verify(cashRadioButton.checked)
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/paymentform/paymentform"'); // Make sure the Cash radio button is checked so we start in the mode // we want to test var cashRadioButton = waitForObject(names.makePaymentCashQRadioButton); if (!cashRadioButton.checked) { clickButton(cashRadioButton); } test.verify(cashRadioButton.checked);
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/paymentform/paymentform\""); # Make sure the Cash radio button is checked so we start in the mode # we want to test my $cashRadioButtonName = {'text'=>'Cash', 'type'=>'QRadioButton', 'visible'=>'1', 'window'=>$Names::make_payment_mainwindow}; my $cashRadioButton = waitForObject($cashRadioButtonName); if (!$cashRadioButton->checked) { clickButton($cashRadioButton); } test::compare($cashRadioButton->checked, 1);
def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/paymentform/paymentform\"") # Make sure the Cash radio button is checked so we start in the mode # we want to test cashRadioButton = waitForObject(Names::Make_Payment_Cash_QRadioButton) if not cashRadioButton.checked clickButton(cashRadioButton) end Test.verify(cashRadioButton.checked)
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/paymentform/paymentform\"" # Make sure the Cash radio button is checked so we start in the mode # we want to test set cashRadioButton [waitForObject $names::Make_Payment_Cash_QRadioButton] if {![property get $cashRadioButton checked]} { invoke clickButton $cashRadioButton } test verify [property get $cashRadioButton checked]
我们必须先确保表单处于我们想要测试的模式。要访问可见的组件项,过程始终相同:我们创建一个变量来持有组件项的名称,然后调用Object waitForObject(objectOrName)以获取组件项的引用。一般情况下,使用符号名最好,但使用具有QObject::setObjectName函数唯一命名的组件项的多属性(真实)名称也很合理,并且当我们需要进行通配符匹配时也有用。
一旦我们有引用,我们可以使用它来访问组件项的属性并调用组件项的方法。我们使用这种方法来检查是否选中了现金单选按钮,如果没有选中,我们就点击它。在任一情况下,我们随后使用Boolean test.compare(value1, value2)方法来确认现金单选按钮已被选中,并确保我们将以正确的模式进行其余的测试。
请注意,clickButton(objectOrName)函数可以用于点击任何继承自QAbstractButton的按钮,即,QCheckBox、QPushButton、QRadioButton和QToolButton的按钮。
# Business rule #1: only the QStackedWidget's CashWidget must be # visible in cash mode # (The name "CashWidget" was set with QObject::setObjectName()) cashWidget = waitForObject({"name": "CashWidget", "type": "QLabel"}) test.compare(cashWidget.visible, True) checkWidgetName = {'name':'CheckWidget', 'type':'QWidget'} # Object is hidden, so we use waitForObjectExists() checkWidget = waitForObjectExists(checkWidgetName) test.compare(checkWidget.visible, False) cardWidgetName = {'name':'CardWidget', 'type':'QWidget'} # Object is hidden, so we use waitForObjectExists() cardWidget = waitForObjectExists(cardWidgetName) test.compare(cardWidget.visible, False)
// Business rule #1: only the QStackedWidget's CashWidget must be // visible in cash mode // (The name "CashWidget" was set with QObject::setObjectName()) var cashWidget = waitForObject({'name':'CashWidget', 'type':'QLabel'}); test.compare(cashWidget.visible, true); var checkWidgetName = {'name':'CheckWidget', 'type':'QWidget'}; // Object is hidden, so we use waitForObjectExists() var checkWidget = waitForObjectExists(checkWidgetName); test.compare(checkWidget.visible, false); var cardWidgetName = {'name':'CardWidget', 'type':'QWidget'}; // Object is hidden, so we use waitForObjectExists() cardWidget = waitForObjectExists(cardWidgetName); test.compare(cardWidget.visible, false);
# Business rule #1: only the QStackedWidget's CashWidget must be # visible in cash mode # (The name "CashWidget" was set with QObject::setObjectName()) my $cashWidget = waitForObject({'name'=>'CashWidget', 'type'=>'QLabel'}); test::compare($cashWidget->visible, 1); my $checkWidgetName = {'name'=>'CheckWidget', 'type'=>'QWidget'}; # Object is hidden, so we use waitForObjectExists() my $checkWidget = waitForObjectExists($checkWidgetName); test::compare($checkWidget->visible, 0); my $cardWidgetName = {'name'=>'CardWidget', 'type'=>'QWidget'}; # Object is hidden, so we use waitForObjectExists() my $cardWidget = waitForObjectExists($cardWidgetName); test::compare($cardWidget->visible, 0);
# Business rule #1: only the QStackedWidget's CashWidget must be # visible in cash mode # (The name "CashWidget" was set with QObject::setObjectName()) cashWidget = waitForObject({:name=>'CashWidget', :type=>'QLabel'}) Test.compare(cashWidget.visible, true) checkWidgetName = {:name=>'CheckWidget', :type=>'QWidget'} # Object is hidden, so we use waitForObjectExists() checkWidget = waitForObjectExists(checkWidgetName) Test.compare(checkWidget.visible, false) cardWidgetName = {:name=>'CardWidget', :type=>'QWidget'} # Object is hidden, so we use waitForObjectExists() cardWidget = waitForObjectExists(cardWidgetName) Test.compare(cardWidget.visible, false)
# Business rule #1: only the QStackedWidget's CashWidget must be # visible in cash mode # (The name "CashWidget" was set with QObject::setObjectName()) set cashWidget [waitForObject [::Squish::ObjectName name CashWidget type QLabel ]] test compare [property get $cashWidget visible] true # Object is hidden, so we use waitForObjectExists() set checkWidget [waitForObjectExists $names::Make_Payment_CheckWidget_QWidget] test compare [property get $checkWidget visible] false # Object is hidden, so we use waitForObjectExists() set cardWidget [waitForObjectExists $names::Make_Payment_CardWidget_QWidget] test compare [property get $cardWidget visible] false
要测试的第一项业务规则是,如果现金组件项可见,则检查和卡组件项必须不可见。通过访问组件项的可见属性来检查组件项是否可见,这与我们用来访问已选中状态的方式完全相同。但对于不可见的组件项,方法略有不同——我们不(并且必须不)调用Object waitForObject(objectOrName);而是立即调用Object findObject(objectName)。我们可以使用类似的方法来检查QTabWidget中的特定标签页组件项或在QToolBox中的特定项组件项是否可见。
# Business rule #2: the minimum payment is $1 and the maximum is # $2000 or the amount due whichever is smaller amountDueLabel = waitForObject({'name':'AmountDueLabel', 'type':'QLabel'}) chars = [] for char in str(amountDueLabel.text): if char.isdigit(): chars.append(char) amount_due = cast("".join(chars), int) maximum = min(2000, amount_due) paymentSpinBoxName = {"buddy": names.make_Payment_This_Payment_QLabel, "type": "QSpinBox", "unnamed": 1} paymentSpinBox = waitForObject(paymentSpinBoxName) test.verify(paymentSpinBox.minimum == 1) test.verify(paymentSpinBox.maximum == maximum)
// Business rule #2: the minimum payment is $1 and the maximum is // $2000 or the amount due whichever is smaller var amountDueLabel = waitForObject({'name':'AmountDueLabel', 'type':'QLabel'}); var amount_due = 0 + String(amountDueLabel.text).replace(/\D/g, ""); var maximum = Math.min(2000, amount_due); var paymentSpinBoxName = {'buddy':names.makePaymentThisPaymentQLabel, 'type':'QSpinBox', 'unnamed':'1'}; var paymentSpinBox = waitForObject(paymentSpinBoxName); test.verify(paymentSpinBox.minimum == 1); test.verify(paymentSpinBox.maximum == maximum);
# Business rule #2: the minimum payment is $1 and the maximum is # $2000 or the amount due whichever is smaller my $amountDueLabel = waitForObject({'name'=>'AmountDueLabel', 'type'=>'QLabel'}); my $amount_due = $amountDueLabel->text; $amount_due =~ s/\D//g; # remove non-digits my $maximum = 2000 < $amount_due ? 2000 : $amount_due; my $paymentSpinBoxName = {'buddy'=>$Names::make_payment_this_payment_qlabel, 'type'=>'QSpinBox', 'unnamed'=>'1'}; my $paymentSpinBox = waitForObject($paymentSpinBoxName); test::verify($paymentSpinBox->minimum == 1); test::verify($paymentSpinBox->maximum == $maximum);
# Business rule #2: the minimum payment is $1 and the maximum is # $2000 or the amount due whichever is smaller amountDueLabel = waitForObject({:name=>'AmountDueLabel', :type=>'QLabel'}) amount_due = String(amountDueLabel.text).gsub(/\D/, "").to_f maximum = 2000 < amount_due ? 2000 : amount_due paymentSpinBoxName = {:buddy=>Names::Make_Payment_This_Payment_QLabel, :type=>'QSpinBox', :unnamed=>'1'} paymentSpinBox = waitForObject(paymentSpinBoxName) Test.verify(paymentSpinBox.minimum == 1) Test.verify(paymentSpinBox.maximum == maximum)
# Business rule #2: the minimum payment is $1 and the maximum is # $2000 or the amount due whichever is smaller set amountDueLabel [waitForObject [::Squish::ObjectName name AmountDueLabel type QLabel]] set amountText [toString [property get $amountDueLabel text]] regsub -all {\D} $amountText "" amountText set amount_due [expr $amountText] set maximum [expr $amount_due < 2000 ? $amount_due : 2000] set paymentSpinBoxName [::Squish::ObjectName type QSpinBox unnamed 1 \ buddy $names::Make_Payment_This_Payment_QLabel ] set paymentSpinBox [waitForObject $paymentSpinBoxName] test compare [property get $paymentSpinBox minimum] 1 test compare [property get $paymentSpinBox maximum] $maximum
第二个业务规则是关于允许的最小和最大支付金额。像往常一样,我们首先使用Object waitForObject(objectOrName)来获取我们想要的组件项的引用——在这种情况下,从应付金额标签开始。这个标签的文本可能包含货币符号和分组标记(例如,$1,700或€1.700),因此为了将其转换为整数,我们首先必须去除所有非数字字符。我们根据底层脚本语言的不同采用不同的方法,但在所有情况下,我们检索标签的text
属性字符并将其转换为整数。(例如,在Python中,我们遍历每个字符,将所有数字字符联合成单个字符串,然后使用Object cast(object, type)函数,它接收一个对象和对象应该转换到的类型,并返回请求类型的对象——或者在失败时返回0。我们在JavaScript中使用类似的方法,但对于Perl和Tcl,我们只需使用正则表达式移除非数字字符。) resulting integer是应付金额,因此现在我们可以轻松计算出可以现金支付的金额上限。
已知最小值和最大值后,我们接下来获取付款旋钮的引用。(注意旋钮没有名称,但通过旁边的标签来唯一标识。)一旦我们获得了旋钮的引用,我们使用布尔测试.verify(condition)方法来确保它设置了正确的最小值和最大值。(对于Tcl,我们使用布尔测试.compare(value1, value2)方法来代替布尔测试.verify(condition),因为这种方法更方便。)
# Business rule #3: the Pay button is enabled (since the above tests # ensure that the payment amount is in range) payButtonName = {"text": "Pay", "type": "QPushButton", "visible": 1} payButton = waitForObject(payButtonName) test.verify(payButton.enabled)
// Business rule #3: the Pay button is enabled (since the above tests // ensure that the payment amount is in range) var payButtonName = {'type':'QPushButton', 'text':'Pay', 'visible':'1'}; var payButton = waitForObject(payButtonName); test.verify(payButton.enabled); sendEvent("QCloseEvent", waitForObject(names.makePaymentMainWindow)); }
# Business rule #3: the Pay button is enabled (since the above tests # ensure that the payment amount is in range) my $payButtonName = {'type'=>'QPushButton', 'text'=>'Pay', 'visible'=>'1'}; my $payButton = waitForObject($payButtonName); test::compare($payButton->enabled, 1); sendEvent("QCloseEvent", waitForObject($Names::make_payment_mainwindow)); }
# Business rule #3: the Pay button is enabled (since the above tests # ensure that the payment amount is in range) payButtonName = {:type=>'QPushButton', :text=>'Pay', :visible=>'1'} payButton = waitForObject(payButtonName) Test.verify(payButton.enabled) sendEvent("QCloseEvent", waitForObject(Names::Make_Payment_MainWindow)) end
# Business rule #3: the Pay button is enabled (since the above tests # ensure that the payment amount is in range) set payButtonName [::Squish::ObjectName text Pay type QPushButton visible 1] set payButton [waitForObject $payButtonName] test verify [property get $payButton enabled] sendEvent "QCloseEvent" [waitForObject $names::Make_Payment_MainWindow] }
在这种情况下,检查最后一个业务规则很简单,因为如果金额在范围内(由于我们刚才已经检查过,它必须是这样),那么允许付款,因此付款按钮应该被启用。同样地,我们使用同样的方法来测试:首先调用对象waitForObject(objectOrName)来获取它的引用,然后进行测试——在本例中,检查付款按钮是否启用。
最后一个测试的一个有趣方面是,如果我们使用间谍工具,它不会给出付款按钮的名称,而是给出包含按钮的QDialogButtonBox的名称,因此我们必须为按钮提供一个对象名称,或者我们自己确定其身份。我们选择了后者,创建了一个属性名字符串,给出了类型、文本(忽略和号)、非命名和可见属性的价值。这足以唯一识别付款按钮。
尽管“现金”模式测试效果良好,但有几个地方我们实际使用的是相同的代码。因此,在创建“支票”模式的测试之前,我们将创建一些通用函数,我们可以使用这些函数来重构我们的测试。(用于创建共享代码的过程将在稍后的如何创建和使用共享数据和共享脚本中简要描述——我们本质上需要做的是在测试套件的共享项目的脚本项下创建一个新的脚本。)Python通用代码在common.py
,JavaScript通用代码在common.js
,等等。我们还创建了一些特定的测试函数,以使main
函数更小、更容易理解——我们将这些函数放在test.py
文件(或test.js
等等)中,在main
函数之上。
import names def clickRadioButton(text): radioButton = waitForObject({'text':text, 'type':'QRadioButton', 'visible':'1', 'window':names.make_Payment_MainWindow}) if not radioButton.checked: clickButton(radioButton) test.verify(radioButton.checked) def getAmountDue(): amountDueLabel = waitForObject({'name':'AmountDueLabel', 'type':'QLabel'}) chars = [] for char in str(amountDueLabel.text): if char.isdigit(): chars.append(char) return cast("".join(chars), int) def checkVisibleWidget(visible, hidden): widget = waitForObject({'name':visible, 'type':'QWidget'}) test.compare(widget.visible, True) for name in hidden: widget = findObject({'name':name, 'type':'QWidget'}) test.compare(widget.visible, False) def checkPaymentRange(minimum, maximum): paymentSpinBox = waitForObject({'buddy':names.make_Payment_This_Payment_QLabel, 'type':'QSpinBox', 'unnamed':'1', 'visible':'1'}) test.verify(paymentSpinBox.minimum == minimum) test.verify(paymentSpinBox.maximum == maximum)
import * as names from 'names.js'; function clickRadioButton(text) { var radioButton = waitForObject({'text':text, 'type':'QRadioButton', "visible":'1', 'window':names.makePaymentMainWindow}); if (!radioButton.checked) { clickButton(radioButton); } test.verify(radioButton.checked); } function getAmountDue() { var amountDueLabel = waitForObject({'name':'AmountDueLabel', 'type':'QLabel'}); return 0 + String(amountDueLabel.text).replace(/\D/g, ""); } function checkVisibleWidget(visible, hidden) { var widget = waitForObject({'name':visible, 'type':'QWidget'}); test.compare(widget.visible, true); for (var i = 0; i < hidden.length; ++i) { var name = hidden[i]; widget = findObject({'name':name, 'type':'QWidget'}); test.compare(widget.visible, false); } } function checkPaymentRange(minimum, maximum) { var paymentSpinBox = waitForObject(names.thisPaymentQSpinBox); test.verify(paymentSpinBox.minimum == minimum); test.verify(paymentSpinBox.maximum == maximum); }
require 'names.pl'; sub clickRadioButton { my $text = shift(@_); my $radioButton = waitForObject({'text'=>$text, 'type'=>'QRadioButton', 'visible'=>'1', 'window'=>$Names::make_payment_mainwindow}); if (!$radioButton->checked) { clickButton($radioButton); } test::verify($radioButton->checked); } sub getAmountDue { my $amountDueLabel = waitForObject({'name'=>'AmountDueLabel', 'type'=>'QLabel'}); my $amount_due = $amountDueLabel->text; $amount_due =~ s/\D//g; # remove non-digits return $amount_due; } sub checkVisibleWidget { my ($visible, @hidden) = @_; my $widget = waitForObject({'name'=>$visible, 'type'=>'QWidget'}); test::compare($widget->visible, 1); foreach (@hidden) { my $widget = findObject({'name'=>$_, 'type'=>'QWidget'}); test::compare($widget->visible, 0); } } sub checkPaymentRange { my ($minimum, $maximum) = @_; my $paymentSpinBox = waitForObject({'buddy'=>$Names::make_payment_this_payment_qlabel, 'type'=>'QSpinBox', 'unnamed'=>'1', 'visible'=>'1'}); test::verify($paymentSpinBox->minimum == $minimum); test::verify($paymentSpinBox->maximum == $maximum); }
# encoding: UTF-8 require 'names' require 'squish' include Squish def clickRadioButton(text) radioButton = waitForObject({:text=>text, :type=>'QRadioButton', :visible=>'1', :window=>Names::Make_Payment_MainWindow}) if not radioButton.checked clickButton(radioButton) end Test.verify(radioButton.checked) end def getAmountDue amountDueLabel = waitForObject({:name=>'AmountDueLabel', :type=>'QLabel'}) String(amountDueLabel.text).gsub(/\D/, "").to_f end def checkVisibleWidget(visible, hidden) widget = waitForObject({:name=>visible, :type=>'QWidget'}) Test.compare(widget.visible, true) for name in hidden widget = findObject({:name=>name, :type=>'QWidget'}) Test.compare(widget.visible, false) end end def checkPaymentRange(minimum, maximum) paymentSpinBox = waitForObject({:buddy=>Names::Make_Payment_This_Payment_QLabel, :type=>'QSpinBox', :unnamed=>'1', :visible=>'1'}) Test.compare(paymentSpinBox.minimum, minimum) Test.compare(paymentSpinBox.maximum, maximum) end def max(x, y) x > y ? x : y end def min(x, y) x < y ? x : y end
source [findFile "scripts" "names.tcl"] proc clickRadioButton {text} { set radioButton [waitForObject [::Squish::ObjectName text $text type QRadioButton visible 1 window $names::Make_Payment_MainWindow]] if (![property get $radioButton checked]) { invoke clickButton $radioButton } test verify [property get $radioButton checked] } proc getAmountDue {} { set amountDueLabel [waitForObject [::Squish::ObjectName name AmountDueLabel type QLabel]] set amountText [toString [property get $amountDueLabel text]] regsub -all {\D} $amountText "" amountText return [expr $amountText] } proc checkVisibleWidget {visible hidden} { set widget [waitForObject [::Squish::ObjectName name $visible type QWidget]] test compare [property get $widget visible] true foreach name $hidden { set widget [findObject [::Squish::ObjectName name $name type QWidget]] test compare [property get $widget visible] false } } proc checkPaymentRange {minimum maximum} { set paymentSpinBox [waitForObject $names::This_Payment_QSpinBox] test compare [property get $paymentSpinBox minimum] $minimum test compare [property get $paymentSpinBox maximum] $maximum }
clickRadioButton
函数用于点击具有给定文本的单选按钮——这用于设置小部件堆栈中的正确页面。getAmoutDue
函数从应付金额标签中读取文本,删除格式化字符(例如,逗号),并将结果转换为整数。checkVisibleWidget
函数检查可见小部件是可见的,隐藏的小部件是不可见的。一个微妙之处在于,对于可见小部件,我们必须始终使用对象waitForObject(objectOrName)函数,而对于隐藏小部件,我们必须不使用它,而是使用对象findObject(objectName)函数代替。最后,checkPaymentRange
函数检查付款旋钮的范围是否与我们期望的范围相匹配。
现在我们可以编写我们的“支票”模式测试,并将更多的精力投入到测试业务规则而不是一些基本任务。我们在test.py
(或test.js
等等)文件中放置的代码被分解为几个函数。main
函数对Squish来说很特殊——这个函数是Squish在测试中调用的唯一函数,因此我们可以自由地添加其他函数,就像我们在这里所做的那样,以使我们的主函数更清晰。
我们将首先展示 main
函数,然后展示它调用的同在 test.py
文件中的函数(因为我们已经看到了从上面的 common.py
调用的函数)。注意,在实际文件中,main
函数位于最后,但为了便于解释,我们更愿意先展示它。
def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/paymentform/paymentform"') # Import functionality needed by more than one test script source(findFile("scripts", "common.py")) # Make sure we start in the mode we want to test: check mode clickRadioButton("Check") # Business rule #1: only the CheckWidget must be visible in check mode checkVisibleWidget("CheckWidget", ("CashWidget", "CardWidget")) # Business rule #2: the minimum payment is $10 and the maximum is # $250 or the amount due whichever is smaller amount_due = getAmountDue() checkPaymentRange(10, min(250, amount_due)) # Business rule #3: the check date must be no earlier than 30 days # ago and no later than tomorrow today = QDate.currentDate() checkDateRange(today.addDays(-30), today.addDays(1)) # Business rule #4: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() payButton = waitForObjectExists(names.make_Payment_Pay_QPushButton) test.verify(not payButton.enabled) # Business rule #5: the check must be signed (and if it isn't we # will check the check box ready to test the next rule) ensureSignedCheckBoxIsChecked() # Business rule #6: the Pay button should be enabled since all the # previous tests pass, the check is signed and now we have filled in # the account details populateCheckFields() payButton = waitForObject(names.make_Payment_Pay_QPushButton) test.verify(payButton.enabled)
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/paymentform/paymentform"'); // Import functionality needed by more than one test script source(findFile("scripts", "common.js")); // Make sure we start in the mode we want to test: check mode clickRadioButton("Check"); // Business rule #1: only the CheckWidget must be visible in check mode checkVisibleWidget("CheckWidget", ["CashWidget", "CardWidget"]); // Business rule #2: the minimum payment is $10 and the maximum is // $250 or the amount due whichever is smaller var amount_due = getAmountDue(); checkPaymentRange(10, Math.min(250, amount_due)); // Business rule #3: the check date must be no earlier than 30 days // ago and no later than tomorrow var today = QDate.currentDate(); checkDateRange(today.addDays(-30), today.addDays(1)); // Business rule #4: the Pay button is disabled (since the form's data // isn't yet valid), so we use waitForObjectExists() var payButton = waitForObjectExists({'type':'QPushButton', 'text':'Pay', 'unnamed':'1', 'visible':'1'}); test.verify(!payButton.enabled); // Business rule #5: the check must be signed (and if it isn't we // will check the check box ready to test the next rule) ensureSignedCheckBoxIsChecked(); // Business rule #6: the Pay button should be enabled since all the // previous tests pass, the check is signed and now we have filled in // the account details populateCheckFields(); payButton = waitForObject({'type':'QPushButton', 'text':'Pay', 'unnamed':'1', 'visible':'1'}); test.verify(payButton.enabled); sendEvent("QCloseEvent", waitForObject(names.makePaymentMainWindow)); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/paymentform/paymentform\""); # Import functionality needed by more than one test script source(findFile("scripts", "common.pl")); # Make sure we start in the mode we want to test: check mode clickRadioButton("Check"); # Business rule #1: only the CheckWidget must be visible in check mode checkVisibleWidget("CheckWidget", ("CashWidget", "CardWidget")); # Business rule #2: the minimum payment is $10 and the maximum is # $250 or the amount due whichever is smaller my $amount_due = getAmountDue(); checkPaymentRange(10, 250 < $amount_due ? 250 : $amount_due); # Business rule #3: the check date must be no earlier than 30 days # ago and no later than tomorrow my $today = QDate::currentDate(); checkDateRange($today->addDays(-30), $today->addDays(1)); # Business rule #4: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() my $payButton = waitForObjectExists({'type'=>'QPushButton', 'text'=>'Pay', 'unnamed'=>'1', 'visible'=>'1'}); test::verify(!$payButton->enabled); # Business rule #5: the check must be signed (and if it isn't we # will check the check box ready to test the next rule) ensureSignedCheckBoxIsChecked(); # Business rule #6: the Pay button should be enabled since all the # previous tests pass, the check is signed and now we have filled in # the account details populateCheckFields(); $payButton = waitForObject({'type'=>'QPushButton', 'text'=>'Pay', 'unnamed'=>'1', 'visible'=>'1'}); test::compare($payButton->enabled, 1); sendEvent("QCloseEvent", waitForObject($Names::make_payment_mainwindow)); }
def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/paymentform/paymentform\"") # Import functionality needed by more than one test script require findFile("scripts", "common.rb") # Make sure we start in the mode we want to test: check mode clickRadioButton("Check") # Business rule #1: only the CheckWidget must be visible in check mode checkVisibleWidget("CheckWidget", ["CashWidget", "CardWidget"]) # Business rule #2: the minimum payment is $10 and the maximum is # $250 or the amount due whichever is smaller amount_due = getAmountDue checkPaymentRange(10, min(250, amount_due)) # Business rule #3: the check date must be no earlier than 30 days # ago and no later than tomorrow today = QDate.currentDate() checkDateRange(today.addDays(-30), today.addDays(1)) # Business rule #4: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() payButton = waitForObjectExists({:type=>'QPushButton', :text=>'Pay', :unnamed=>'1', :visible=>'1'}) Test.verify(!payButton.enabled) # Business rule #5: the check must be signed (and if it isn't we # will check the check box ready to test the next rule) ensureSignedCheckBoxIsChecked # Business rule #6: the Pay button should be enabled since all the # previous tests pass, the check is signed and now we have filled in # the account details populateCheckFields payButton = waitForObject({:type=>'QPushButton', :text=>'Pay', :unnamed=>'1', :visible=>'1'}) Test.verify(payButton.enabled) sendEvent("QCloseEvent", waitForObject(Names::Make_Payment_MainWindow)) end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/paymentform/paymentform\"" # Import functionality needed by more than one test script source [findFile "scripts" "common.tcl"] # Make sure we start in the mode we want to test: check mode clickRadioButton "Check" # Business rule #1: only the CheckWidget must be visible in check mode checkVisibleWidget "CheckWidget" {"CashWidget" "CardWidget"} # Business rule #2: the minimum payment is $10 and the maximum is # $250 or the amount due whichever is smaller set amount_due [getAmountDue] set maximum [expr 250 > $amount_due ? $amount_due : 250] checkPaymentRange 10 $maximum # Business rule #3: the check date must be no earlier than 30 days # ago and no later than tomorrow set today [invoke QDate currentDate] set thirtyDaysAgo [toString [invoke $today addDays -30]] set tomorrow [toString [invoke $today addDays 1]] checkDateRange $thirtyDaysAgo $tomorrow # Business rule #4: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() set payButton [waitForObjectExists $names::Make_Payment_Pay_QPushButton] test verify [expr ![property get $payButton enabled]] # Business rule #5: the check must be signed (and if it isn't we # will check the check box ready to test the next rule) ensureSignedCheckBoxIsChecked # Business rule #6: the Pay button should be enabled since all the # previous tests pass, the check is signed and now we have filled in # the account details populateCheckFields set payButton [waitForObject $names::Make_Payment_Pay_QPushButton] test verify [property get $payButton enabled] sendEvent "QCloseEvent" [waitForObject $names::Make_Payment_MainWindow] }
使用 source(filename) 函数读取脚本并执行它。(Ruby 用户可以使用标准的 require
函数代替。)通常,这样的脚本纯粹用于定义事物——例如,函数——然后这些函数就可供测试脚本使用。
有了我们定制的 clickRadioButton
函数,现在把表单放入正确模式只是一件简单的事情。
所有的业务规则与之前相似,但是由于我们使用了通用函数(clickRadioButton
、checkVisibleWidget
、getAmoutDue
、和 checkPaymentRange
)和特定的测试函数(checkDateRange
、populateCheckFields
和 ensureSignedCheckBoxIsChecked
),每个测试规则的代码都已减少到一行或两行。以下展示了这些辅助函数,每个函数后面都有一个简要说明。
def checkDateRange(minimum, maximum): checkDateEdit = waitForObject(names.check_Date_QDateEdit) test.verify(checkDateEdit.minimumDate == minimum) test.verify(checkDateEdit.maximumDate == maximum)
function checkDateRange(minimum, maximum) { var checkDateEdit = waitForObject({buddy:names.makePaymentCheckDateQLabel, 'type':'QDateEdit', 'unnamed':'1', 'visible':'1'}); test.verify(checkDateEdit.minimumDate == minimum); test.verify(checkDateEdit.maximumDate == maximum); }
sub checkDateRange { my ($minimum, $maximum) = @_; $checkDateEdit = waitForObject({'buddy'=>$Names::make_payment_check_date_qlabel, 'type'=>'QDateEdit', 'unnamed'=>'1', 'visible'=>'1'}); test::verify($checkDateEdit->minimumDate == $minimum); test::verify($checkDateEdit->maximumDate == $maximum); }
def checkDateRange(minimum, maximum) checkDateEdit = waitForObject({:buddy=>Names::Make_Payment_Check_Date_QLabel, :type=>'QDateEdit', :unnamed=>'1', :visible=>'1'}) Test.verify(checkDateEdit.minimumDate == minimum) Test.verify(checkDateEdit.maximumDate == maximum) end
proc checkDateRange {minimum maximum} { set checkDateEdit [waitForObject [$names::Check_Date_QDateEdit]] set minimumDate [toString [property get $checkDateEdit minimumDate]] set maximumDate [toString [property get $checkDateEdit maximumDate]] test verify [string equal $minimum $minimumDate] test verify [string equal $maximum $maximumDate] }
checkDateRange
函数展示了我们如何测试 QDateEdit 的属性。(注意:Tcl 用户的注意:我们通过将它们转换为字符串来比较日期。)
def ensureSignedCheckBoxIsChecked(): checkSignedCheckBox = waitForObject(names.make_Payment_Check_Signed_QCheckBox) if not checkSignedCheckBox.checked: clickButton(checkSignedCheckBox) test.verify(checkSignedCheckBox.checked)
function ensureSignedCheckBoxIsChecked() { var checkSignedCheckBox = waitForObject({'text':'Check Signed', 'type':'QCheckBox', 'unnamed':'1', 'visible':'1', 'window':names.makePaymentMainWindow}); if (!checkSignedCheckBox.checked) { clickButton(checkSignedCheckBox); } test.verify(checkSignedCheckBox.checked); }
sub ensureSignedCheckBoxIsChecked { my $checkSignedCheckBox = waitForObject({'text'=>'Check Signed', 'type'=>'QCheckBox', 'unnamed'=>'1', 'visible'=>'1', 'window'=>$Names::make_payment_mainwindow}); if (!$checkSignedCheckBox->checked) { clickButton($checkSignedCheckBox); } test::verify($checkSignedCheckBox->checked); }
def ensureSignedCheckBoxIsChecked checkSignedCheckBox = waitForObject({:text=>'Check Signed', :type=>'QCheckBox', :unnamed=>'1', :visible=>'1', :window=>Names::Make_Payment_MainWindow}) if not checkSignedCheckBox.checked clickButton(checkSignedCheckBox) end Test.verify(checkSignedCheckBox.checked) end
proc ensureSignedCheckBoxIsChecked {} { set checkSignedCheckBox [waitForObject $names::Make_Payment_Check_Signed_QCheckBox] if (![property get $checkSignedCheckBox checked]) { invoke clickButton $checkSignedCheckBox } test verify [property get $checkSignedCheckBox checked] }
ensureSignedCheckBoxIsChecked
函数检查复选框是否未被选中——然后它验证该复选框是否被选中。
def populateCheckFields(): bankNameLineEdit = waitForObject(names.bank_Name_QLineEdit) type(bankNameLineEdit, "A Bank") bankNumberLineEdit = waitForObject(names.bank_Number_QLineEdit_2) type(bankNumberLineEdit, "88-91-33X") accountNameLineEdit = waitForObject(names.account_Name_QLineEdit_2) type(accountNameLineEdit, "An Account") accountNumberLineEdit = waitForObject(names.account_Number_QLineEdit_2) type(accountNumberLineEdit, "932745395")
function populateCheckFields() { var bankNameLineEdit = waitForObject({'buddy':names.makePaymentBankNameQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(bankNameLineEdit, "A Bank"); var bankNumberLineEdit = waitForObject({'buddy':names.makePaymentBankNumberQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(bankNumberLineEdit, "88-91-33X"); var accountNameLineEdit = waitForObject({'buddy':names.makePaymentAccountNameQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(accountNameLineEdit, "An Account"); var accountNumberLineEdit = waitForObject({'buddy':names.makePaymentAccountNumberQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(accountNumberLineEdit, "932745395"); }
sub populateCheckFields { my $bankNameLineEdit = waitForObject({'buddy'=>$Names::make_payment_bank_name_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($bankNameLineEdit, "A Bank"); my $bankNumberLineEdit = waitForObject({'buddy'=>$Names::make_payment_bank_number_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($bankNumberLineEdit, "88-91-33X"); my $accountNameLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_name_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($accountNameLineEdit, "An Account"); my $accountNumberLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_number_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($accountNumberLineEdit, "932745395"); }
def populateCheckFields bankNameLineEdit = waitForObject({:buddy=>Names::Make_Payment_Bank_Name_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(bankNameLineEdit, "A Bank") bankNumberLineEdit = waitForObject({:buddy=>Names::Make_Payment_Bank_Number_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(bankNumberLineEdit, "88-91-33X") accountNameLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Name_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(accountNameLineEdit, "An Account") accountNumberLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Number_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(accountNumberLineEdit, "932745395") end
proc populateCheckFields {} { set bankNameLineEdit [waitForObject $names::Bank_Name_QLineEdit] invoke type $bankNameLineEdit "A Bank" set bankNumberLineEdit [waitForObject $names::Bank_Number_QLineEdit] invoke type $bankNumberLineEdit "88-91-33X" set accountNameLineEdit [waitForObject $names::Account_Name_QLineEdit] invoke type $accountNameLineEdit "An Account" set accountNumberLineEdit [waitForObject $names::Account_Number_QLineEdit] invoke type $accountNumberLineEdit "932745395" }
populateCheckFields
函数使用 type(objectOrName, text) 函数来模拟用户输入文本。通常,模拟用户交互比直接设置小部件属性要好——毕竟,我们通常想要测试的是用户体验中的应用程序行为。一旦字段被填写,Pay 按钮应该被启用,并在调用 populateCheckFields
函数后在 main
函数的第六个业务规则中检查此点。
另一个需要注意的点是,在这个表单中,我们有两个都没有命名的线编辑,标签都是“Account Name”,另外两个标签是“Account Number”。因为任何时候只有一个可见,Squish 能够区分它们。当然,如果我们想要,我们可以在 AUT 的源代码中使用 QObject::setObjectName
方法给它们唯一的名称。
def checkDateRange(minimum, maximum): checkDateEdit = waitForObject(names.check_Date_QDateEdit) test.verify(checkDateEdit.minimumDate == minimum) test.verify(checkDateEdit.maximumDate == maximum) def ensureSignedCheckBoxIsChecked(): checkSignedCheckBox = waitForObject(names.make_Payment_Check_Signed_QCheckBox) if not checkSignedCheckBox.checked: clickButton(checkSignedCheckBox) test.verify(checkSignedCheckBox.checked) def populateCheckFields(): bankNameLineEdit = waitForObject(names.bank_Name_QLineEdit) type(bankNameLineEdit, "A Bank") bankNumberLineEdit = waitForObject(names.bank_Number_QLineEdit_2) type(bankNumberLineEdit, "88-91-33X") accountNameLineEdit = waitForObject(names.account_Name_QLineEdit_2) type(accountNameLineEdit, "An Account") accountNumberLineEdit = waitForObject(names.account_Number_QLineEdit_2) type(accountNumberLineEdit, "932745395")
function checkDateRange(minimum, maximum) { var checkDateEdit = waitForObject({buddy:names.makePaymentCheckDateQLabel, 'type':'QDateEdit', 'unnamed':'1', 'visible':'1'}); test.verify(checkDateEdit.minimumDate == minimum); test.verify(checkDateEdit.maximumDate == maximum); } function ensureSignedCheckBoxIsChecked() { var checkSignedCheckBox = waitForObject({'text':'Check Signed', 'type':'QCheckBox', 'unnamed':'1', 'visible':'1', 'window':names.makePaymentMainWindow}); if (!checkSignedCheckBox.checked) { clickButton(checkSignedCheckBox); } test.verify(checkSignedCheckBox.checked); } function populateCheckFields() { var bankNameLineEdit = waitForObject({'buddy':names.makePaymentBankNameQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(bankNameLineEdit, "A Bank"); var bankNumberLineEdit = waitForObject({'buddy':names.makePaymentBankNumberQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(bankNumberLineEdit, "88-91-33X"); var accountNameLineEdit = waitForObject({'buddy':names.makePaymentAccountNameQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(accountNameLineEdit, "An Account"); var accountNumberLineEdit = waitForObject({'buddy':names.makePaymentAccountNumberQLabel, 'type':'QLineEdit', 'unnamed':'1', 'visible':'1'}); type(accountNumberLineEdit, "932745395"); }
sub checkDateRange { my ($minimum, $maximum) = @_; $checkDateEdit = waitForObject({'buddy'=>$Names::make_payment_check_date_qlabel, 'type'=>'QDateEdit', 'unnamed'=>'1', 'visible'=>'1'}); test::verify($checkDateEdit->minimumDate == $minimum); test::verify($checkDateEdit->maximumDate == $maximum); } sub ensureSignedCheckBoxIsChecked { my $checkSignedCheckBox = waitForObject({'text'=>'Check Signed', 'type'=>'QCheckBox', 'unnamed'=>'1', 'visible'=>'1', 'window'=>$Names::make_payment_mainwindow}); if (!$checkSignedCheckBox->checked) { clickButton($checkSignedCheckBox); } test::verify($checkSignedCheckBox->checked); } sub populateCheckFields { my $bankNameLineEdit = waitForObject({'buddy'=>$Names::make_payment_bank_name_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($bankNameLineEdit, "A Bank"); my $bankNumberLineEdit = waitForObject({'buddy'=>$Names::make_payment_bank_number_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($bankNumberLineEdit, "88-91-33X"); my $accountNameLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_name_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($accountNameLineEdit, "An Account"); my $accountNumberLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_number_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($accountNumberLineEdit, "932745395"); }
def checkDateRange(minimum, maximum) checkDateEdit = waitForObject({:buddy=>Names::Make_Payment_Check_Date_QLabel, :type=>'QDateEdit', :unnamed=>'1', :visible=>'1'}) Test.verify(checkDateEdit.minimumDate == minimum) Test.verify(checkDateEdit.maximumDate == maximum) end def ensureSignedCheckBoxIsChecked checkSignedCheckBox = waitForObject({:text=>'Check Signed', :type=>'QCheckBox', :unnamed=>'1', :visible=>'1', :window=>Names::Make_Payment_MainWindow}) if not checkSignedCheckBox.checked clickButton(checkSignedCheckBox) end Test.verify(checkSignedCheckBox.checked) end def populateCheckFields bankNameLineEdit = waitForObject({:buddy=>Names::Make_Payment_Bank_Name_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(bankNameLineEdit, "A Bank") bankNumberLineEdit = waitForObject({:buddy=>Names::Make_Payment_Bank_Number_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(bankNumberLineEdit, "88-91-33X") accountNameLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Name_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(accountNameLineEdit, "An Account") accountNumberLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Number_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(accountNumberLineEdit, "932745395") end
proc checkDateRange {minimum maximum} { set checkDateEdit [waitForObject [$names::Check_Date_QDateEdit]] set minimumDate [toString [property get $checkDateEdit minimumDate]] set maximumDate [toString [property get $checkDateEdit maximumDate]] test verify [string equal $minimum $minimumDate] test verify [string equal $maximum $maximumDate] } proc ensureSignedCheckBoxIsChecked {} { set checkSignedCheckBox [waitForObject $names::Make_Payment_Check_Signed_QCheckBox] if (![property get $checkSignedCheckBox checked]) { invoke clickButton $checkSignedCheckBox } test verify [property get $checkSignedCheckBox checked] } proc populateCheckFields {} { set bankNameLineEdit [waitForObject $names::Bank_Name_QLineEdit] invoke type $bankNameLineEdit "A Bank" set bankNumberLineEdit [waitForObject $names::Bank_Number_QLineEdit] invoke type $bankNumberLineEdit "88-91-33X" set accountNameLineEdit [waitForObject $names::Account_Name_QLineEdit] invoke type $accountNameLineEdit "An Account" set accountNumberLineEdit [waitForObject $names::Account_Number_QLineEdit] invoke type $accountNumberLineEdit "932745395" }
现在,我们已经准备好查看表单业务逻辑的最后测试——即测试“卡”模式。就像“支票”模式一样,我们通过使用在 common.py
文件(或 common.js
等等)中定义的函数,以及在 test.py
文件(或 test.js
和等等)中使用特定的测试函数,来缩短并简化了 main
函数。
def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/paymentform/paymentform"') source(findFile("scripts", "common.py")) # Make sure we start in the mode we want to test: card mode clickRadioButton("Credit Card") # Business rule #1: only the CardWidget must be visible in check mode checkVisibleWidget("CardWidget", ("CashWidget", "CheckWidget")) # Business rule #2: the minimum payment is $10 or 5% of the amount due # whichever is larger and the maximum is $5000 or the amount due # whichever is smaller amount_due = getAmountDue() checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due)) # Business rule #3: for non-Visa cards the issue date must be no # earlier than 3 years ago # Business rule #4: the expiry date must be at least a month later # than today---we will make sure this is the case for the later tests checkCardDateEdits() # Business rule #5: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() payButton = waitForObjectExists(names.make_Payment_Pay_QPushButton) test.compare(payButton.enabled, False) # Business rule #6: the Pay button should be enabled since all the # previous tests pass, and now we have filled in the account details populateCardFields() payButton = waitForObject(names.make_Payment_Pay_QPushButton) test.verify(payButton.enabled)
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/paymentform/paymentform"'); source(findFile("scripts", "common.js")); // Make sure we start in the mode we want to test: card mode clickRadioButton("Credit Card"); // Business rule #1: only the CardWidget must be visible in check mode checkVisibleWidget("CardWidget", ["CashWidget", "CheckWidget"]); // Business rule #2: the minimum payment is $10 or 5% of the amount due // whichever is larger and the maximum is $5000 or the amount due // whichever is smaller var amount_due = getAmountDue(); checkPaymentRange(Math.max(10, amount_due / 20.0), Math.min(5000, amount_due)); // Business rule #3: for non-Visa cards the issue date must be no // earlier than 3 years ago // Business rule #4: the expiry date must be at least a month later // than today---we will make sure this is the case for the later tests checkCardDateEdits(); // Business rule #5: the Pay button is disabled (since the form's data // isn't yet valid), so we use waitForObjectExists() var payButton = waitForObjectExists(names.makePaymentPayQPushButton); test.compare(payButton.enabled, false); // Business rule #6: the Pay button should be enabled since all the // previous tests pass, and now we have filled in the account details populateCardFields(); payButton = waitForObject(names.makePaymentPayQPushButton); test.verify(payButton.enabled); sendEvent("QCloseEvent", waitForObject(names.makePaymentMainWindow)); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/paymentform/paymentform\""); source(findFile("scripts", "common.pl")); # Make sure we start in the mode we want to test: card mode clickRadioButton("Credit Card"); # Business rule #1: only the CardWidget must be visible in check mode checkVisibleWidget("CardWidget", ("CashWidget", "CheckWidget")); # Business rule #2: the minimum payment is $10 or 5% of the amount due # whichever is larger and the maximum is $5000 or the amount due # whichever is smaller my $amount_due = getAmountDue(); my $paymentSpinBox = waitForObject({'buddy'=>$Names::make_payment_this_payment_qlabel, 'type'=>'QSpinBox', 'unnamed'=>'1', 'visible'=>'1'}); my $fraction = $amount_due / 20.0; checkPaymentRange(10 < $fraction ? $fraction : 10, 5000 < $amount_due ? 5000 : $amount_due); # Business rule #3: for non-Visa cards the issue date must be no # earlier than 3 years ago # Business rule #4: the expiry date must be at least a month later # than today---we will make sure this is the case for the later tests checkCardDateEdits(); # Business rule #5: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() my $payButton = waitForObjectExists({'type'=>'QPushButton', 'text'=>'Pay', 'unnamed'=>'1', 'visible'=>'1'}); test::compare($payButton->enabled, 0); # Business rule #6: the Pay button should be enabled since all the # previous tests pass, and now we have filled in the account details populateCardFields(); $payButton = waitForObject({'type'=>'QPushButton', 'text'=>'Pay', 'unnamed'=>'1', 'visible'=>'1'}); test::compare($payButton->enabled, 1); sendEvent("QCloseEvent", waitForObject($Names::make_payment_mainwindow)); }
def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/paymentform/paymentform\"") require findFile("scripts", "common.rb") # Make sure we start in the mode we want to test: card mode clickRadioButton("Credit Card") # Business rule #1: only the CardWidget must be visible in check mode checkVisibleWidget("CardWidget", ["CashWidget", "CheckWidget"]) # Business rule #2: the minimum payment is $10 or 5% of the amount due # whichever is larger and the maximum is $5000 or the amount due # whichever is smaller amount_due = getAmountDue checkPaymentRange(max(10, amount_due / 20.0), min(5000, amount_due)) # Business rule #3: for non-Visa cards the issue date must be no # earlier than 3 years ago # Business rule #4: the expiry date must be at least a month later # than today---we will make sure this is the case for the later tests checkCardDateEdits # Business rule #5: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() payButton = waitForObjectExists({:type=>'QPushButton', :text=>'Pay', :unnamed=>'1'}) Test.compare(payButton.enabled, false) # Business rule #6: the Pay button should be enabled since all the # previous tests pass, and now we have filled in the account details populateCardFields payButton = waitForObject({:type=>'QPushButton', :text=>'Pay', :unnamed=>'1'}) Test.verify(payButton.enabled) sendEvent("QCloseEvent", waitForObject(Names::Make_Payment_MainWindow)) end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/paymentform/paymentform\"" source [findFile "scripts" "common.tcl"] # Make sure we start in the mode we want to test: card mode clickRadioButton "Credit Card" # Business rule #1: only the CardWidget must be visible in check mode checkVisibleWidget "CardWidget" {"CashWidget" "CheckWidget"} # Business rule #2: the minimum payment is $10 or 5% of the amount due # whichever is larger and the maximum is $5000 or the amount due # whichever is smaller set amount_due [getAmountDue] set five_percent [expr $amount_due / 20.0] set minimum [expr 10 < $five_percent ? $five_percent : 10] set maximum [expr 5000 > $amount_due ? $amount_due : 5000] checkPaymentRange $minimum $maximum # Business rule #3: for non-Visa cards the issue date must be no # earlier than 3 years ago # Business rule #4: the expiry date must be at least a month later # than today---we will make sure this is the case for the later tests checkCardDateEdits # Business rule #5: the Pay button is disabled (since the form's data # isn't yet valid), so we use waitForObjectExists() set payButton [waitForObjectExists $names::Make_Payment_Pay_QPushButton] test compare [property get $payButton enabled] false # Business rule #6: the Pay button should be enabled since all the # previous tests pass, and now we have filled in the account details populateCardFields set payButton [waitForObject $names::Make_Payment_Pay_QPushButton] test verify [property get $payButton enabled] sendEvent "QCloseEvent" [waitForObject $names::Make_Payment_MainWindow] }
对每个业务规则的测试与我们为“支票”模式所做的非常相似——例如,业务规则一和二使用了相同的函数,但参数不同。我们将业务规则三和四的测试组合成了一个单一的特定测试函数 checkCardDateEdits
,我们稍后会看到。业务规则五和六的工作方式与之前完全相同,只是这次我们必须填写不同的小部件来启用 Pay 按钮并创建了特定的测试函数 populateCardFields
来完成此操作。
def checkCardDateEdits(): cardTypeComboBox = waitForObject(names.card_Type_QComboBox) for index in range(cardTypeComboBox.count): if cardTypeComboBox.itemText(index) != "Visa": cardTypeComboBox.setCurrentIndex(index) break today = QDate.currentDate() issueDateEdit = waitForObject(names.issue_Date_QDateEdit) test.verify(issueDateEdit.minimumDate == today.addYears(-3)) expiryDateEdit = waitForObject(names.expiry_Date_QDateEdit) type(expiryDateEdit, today.addMonths(2).toString("MMM yyyy")) def populateCardFields(): cardAccountNameLineEdit = waitForObject(names.account_Name_QLineEdit_2) type(cardAccountNameLineEdit, "An Account") cardAccountNumberLineEdit = waitForObject(names.account_Number_QLineEdit_2) type(cardAccountNumberLineEdit, "1343 876 326 1323 32")
function checkCardDateEdits() { var cardTypeComboBox = waitForObject(names.cardTypeQComboBox); for (var index = 0; index < cardTypeComboBox.count; ++index) { if (cardTypeComboBox.itemText(index) != "Visa") { cardTypeComboBox.setCurrentIndex(index); break; } } var today = QDate.currentDate(); var issueDateEdit = waitForObject(names.issueDateQDateEdit); test.verify(issueDateEdit.minimumDate == today.addYears(-3)); var expiryDateEdit = waitForObject(names.expiryDateQDateEdit); type(expiryDateEdit, today.addMonths(2).toString("MMM yyyy")); } function populateCardFields() { var cardAccountNameLineEdit = waitForObject(names.accountNameQLineEdit); type(cardAccountNameLineEdit, "An Account"); var cardAccountNumberLineEdit = waitForObject(names.accountNumberQLineEdit); type(cardAccountNumberLineEdit, "1343 876 326 1323 32"); }
sub checkCardDateEdits { my $cardTypeComboBox = waitForObject({'buddy'=>$Names::make_payment_card_type_qlabel, 'type'=>'QComboBox', 'unnamed'=>'1', 'visible'=>'1'}); for (my $index = 0; $index < $cardTypeComboBox->count; $index++) { if ($cardTypeComboBox->itemText($index) != "Visa") { $cardTypeComboBox->setCurrentIndex($index); last; } } my $today = QDate::currentDate(); my $issueDateEdit = waitForObject({'buddy'=>$Names::make_payment_issue_date_qlabel, 'type'=>'QDateEdit', 'unnamed'=>'1', 'visible'=>'1'}); test::verify($issueDateEdit->minimumDate == $today->addYears(-3)); my $expiryDateEdit = waitForObject({'buddy'=>$Names::make_payment_expiry_date_qlabel, 'type'=>'QDateEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($expiryDateEdit, $today->addMonths(2)->toString("MMM yyyy")); } sub populateCardFields { my $cardAccountNameLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_name_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($cardAccountNameLineEdit, "An Account"); my $cardAccountNumberLineEdit = waitForObject({'buddy'=>$Names::make_payment_account_number_qlabel, 'type'=>'QLineEdit', 'unnamed'=>'1', 'visible'=>'1'}); type($cardAccountNumberLineEdit, "1343 876 326 1323 32"); }
def checkCardDateEdits cardTypeComboBox = waitForObject({:buddy=>Names::Make_Payment_Card_Type_QLabel, :type=>'QComboBox', :unnamed=>'1', :visible=>'1'}) for index in 0...cardTypeComboBox.count if cardTypeComboBox.itemText(index) != "Visa" cardTypeComboBox.setCurrentIndex(index) break end end today = QDate.currentDate() issueDateEdit = waitForObject({:buddy=>Names::Make_Payment_Issue_Date_QLabel, :type=>'QDateEdit', :unnamed=>'1', :visible=>'1'}) Test.verify(issueDateEdit.minimumDate == today.addYears(-3)) expiryDateEdit = waitForObject({:buddy=>Names::Make_Payment_Expiry_Date_QLabel, :type=>'QDateEdit', :unnamed=>'1', :visible=>'1'}) type(expiryDateEdit, today.addMonths(2).toString("MMM yyyy")) end def populateCardFields cardAccountNameLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Name_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(cardAccountNameLineEdit, "An Account") cardAccountNumberLineEdit = waitForObject({:buddy=>Names::Make_Payment_Account_Number_QLabel, :type=>'QLineEdit', :unnamed=>'1', :visible=>'1'}) type(cardAccountNumberLineEdit, "1343 876 326 1323 32") end
proc checkCardDateEdits {} { set cardTypeComboBox [waitForObject $names::Card_Type_QComboBox] set count [property get $cardTypeComboBox count] for {set index 0} {$index < $count} {incr index} { if {[invoke $cardTypeComboBox itemText $index] != "Visa"} { invoke $cardTypeComboBox setCurrentIndex $index break } } set today [invoke QDate currentDate] set issueDateEdit [waitForObject $names::Issue_Date_QDateEdit] set maximumIssueDate [toString [property get $issueDateEdit \ maximumDate]] set threeYearsAgo [toString [invoke $today addYears -3]] test verify [string equal $maximumIssueDate $threeYearsAgo] set expiryDateEdit [waitForObject $names::Expiry_Date_QDateEdit] set date [invoke $today addMonths 2] invoke type $expiryDateEdit [invoke $date toString "MMM yyyy"] } proc populateCardFields {} { set cardAccountNameLineEdit [waitForObject $names::Account_Name_QLineEdit] invoke type $cardAccountNameLineEdit "An Account" set cardAccountNumberLineEdit [waitForObject $names::Account_Number_QLineEdit] invoke type $cardAccountNumberLineEdit "1343 876 326 1323 32" }
《checkCardDateEdits》函数用于业务规则三和四。对于规则三,我们需要将信用卡类型组合框设置为除维萨卡以外的任何卡类型,因此我们遍历组合框中的项并将当前项设置为发现的第一个非维萨卡项。然后检查最小发行日期是否已正确设置为三年前。业务规则四指定到期日期必须至少提前一个月。我们明确将到期日期提前几个月,以便稍后启用Pay按钮。然而,初始时,应该禁用Pay按钮,所以在《main》函数中的业务规则五的代码会进行这一检查。
对于最后一个业务规则,我们需要一些关于卡账户名称和号码的假数据,这正是《populateCardFields》函数所生成的。在调用此函数并确保日期在《checkCardDateEdits》函数中符合范围之后,现在应启用Pay按钮。在《main》函数的末尾我们会检查这一点。
我们已经完成了使用有状态单值部件测试业务规则的审查。Qt 有其他这样的部件,包括QDateTimeEdit、QDial、QDoubleSpinBox 和 QTimeEdit,但它们都使用我们在这里看到的技术进行识别和测试。
如何在项目视图中测试项目
在本节中,我们将了解如何迭代 Qt 项目部件(例如 QListWidget、QTableWidget 和 QTreeWidget)中的每个项目,Qt 的项目视图(例如 QListView、QTableView 和 QTreeView),以及提取每个项目的文本并检查其选中状态和是否被选中。事实上,对于《Q*View》类,我们访问其底层的模型,(例如 QAbstractItemModel、QAbstractTableModel 或,QStandardItemModel),并迭代模型的数据,因为视图本身仅显示数据而不实际持有数据。
尽管示例将每个项目的文本、选中状态和选中状态输出到 Squish 的日志中,但它们很容易修改进行更复杂的测试,例如将实际值与预期值进行比较。有一个特定的例外,本节中展示的所有代码均来自《examples/qt/itemviews》示例的测试套件。
如何在QListWidget中测试项目
如以下测试示例所示,遍历列表部件中的所有项目并获取它们的文本以及检查它们的选中状态是非常容易的。
import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/itemviews/itemviews"') listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}" listWidget = waitForObject(listWidgetName) for row in range(listWidget.count): item = listWidget.item(row) checked = selected = "" if item.checkState() == Qt.Checked: checked = " +checked" if item.isSelected(): selected = " +selected" test.log("(%d) '%s'%s%s" % (row, item.text(), checked, selected)) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow"))
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/itemviews/itemviews"'); var listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}"; var listWidget = waitForObject(listWidgetName); for (var row = 0; row < listWidget.count; ++row) { var item = listWidget.item(row); var checked = ""; var selected = ""; if (item.checkState() == Qt.Checked) { checked = " +checked"; } if (item.isSelected()) { selected = " +selected"; } test.log("(" + String(row) + ") '" + item.text() + "'" + checked + selected); } sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/itemviews/itemviews\""); my $listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}"; my $listWidget = waitForObject($listWidgetName); for ( my $row = 0 ; $row < $listWidget->count ; ++$row ) { my $item = $listWidget->item($row); my $checked = ""; my $selected = ""; if ( $item->checkState() == Qt::Checked ) { $checked = " +checked"; } if ( $item->isSelected() ) { $selected = " +selected"; } test::log( "($row) '" . $item->text() . "'$checked$selected" ); } sendEvent( "QCloseEvent", waitForObject(":Item Views_MainWindow") ); }
# encoding: UTF-8 require 'squish' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/itemviews/itemviews\"") listWidgetName = "{type='QListWidget' unnamed='1' visible='1'}" listWidget = waitForObject(listWidgetName) for row in 0...listWidget.count do item = listWidget.item(row) checked = selected = "" if item.checkState() == Qt::CHECKED checked = " +checked" end if item.isSelected() selected = " +selected" end Test.log("(#{row}) '#{item.text()}'#{checked}#{selected}") end sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")) end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/itemviews/itemviews\"" set listWidgetName {{type='QListWidget' unnamed='1' visible='1'}} set listWidget [waitForObject $listWidgetName] for {set row 0} {$row < [property get $listWidget count]} {incr row} { set item [invoke $listWidget item $row] set checked "" set selected "" if {[invoke $item checkState] == [enum Qt Checked]} { set checked " +checked" } if [invoke $item isSelected] { set selected " +selected" } set text [toString [invoke $item text]] test log "($row) '$text'$checked$selected" } sendEvent "QCloseEvent" [waitForObject ":Item Views_MainWindow"] }
所有输出都送入 Squish 的日志中,但很明显可以修改脚本以测试特定的值列表等。
如何在QListView中测试项目(QAbstractItemModel 和 QItemSelectionModel)
视图类本身不保存任何数据;相反,它们将模型中保存的数据可视化。因此,如果我们想访问与视图关联的所有项目,我们首先必须检索视图的模型,然后遍历模型的项目。另外,选择与数据模型分开保存——在选择模型中。这是因为选择涉及视觉交互,并且不影响底层数据。(当然,用户可能会做出选择然后对选择应用更改,但从数据模型的角度来看,更改只是简单地应用于一个或多个项目,模型不知道或不关心这些项目是如何选择的。)
import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/itemviews/itemviews"') listViewName = "{type='QListView' unnamed='1' visible='1'}" listView = waitForObject(listViewName) model = listView.model() selectionModel = listView.selectionModel() for row in range(model.rowCount()): index = model.index(row, 0) text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt.CheckStateRole).toInt() if checkState == Qt.Checked: checked = " +checked" if selectionModel.isSelected(index): selected = " +selected" test.log("(%d) '%s'%s%s" % (row, text, checked, selected)) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow"))
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/itemviews/itemviews"'); var listViewName = "{type='QListView' unnamed='1' visible='1'}"; var listView = waitForObject(listViewName); var model = listView.model(); var selectionModel = listView.selectionModel(); for (var row = 0; row < model.rowCount(); ++row) { var index = model.index(row, 0); var text = model.data(index).toString(); var checked = ""; var selected = ""; var checkState = model.data(index, Qt.CheckStateRole).toInt(); if (checkState == Qt.Checked) { checked = " +checked"; } if (selectionModel.isSelected(index)) { selected = " +selected"; } test.log("(" + String(row) + ") '" + text + "'" + checked + selected); } sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/itemviews/itemviews\""); my $listViewName = "{type='QListView' unnamed='1' visible='1'}"; my $listView = waitForObject($listViewName); my $model = $listView->model(); my $selectionModel = $listView->selectionModel(); for ( my $row = 0 ; $row < $model->rowCount() ; ++$row ) { my $index = $model->index( $row, 0 ); my $text = $model->data($index)->toString(); my $checked = ""; my $selected = ""; my $checkState = $model->data( $index, Qt::CheckStateRole )->toInt(); if ( $checkState == Qt::Checked ) { $checked = " +checked"; } if ( $selectionModel->isSelected($index) ) { $selected = " +selected"; } test::log("($row) '$text'$checked$selected"); } sendEvent( "QCloseEvent", waitForObject(":Item Views_MainWindow") ); }
# encoding: UTF-8 require 'squish' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/itemviews/itemviews\"") listViewName = "{type='QListView' unnamed='1' visible='1'}" listView = waitForObject(listViewName) model = listView.model() selectionModel = listView.selectionModel() for row in 0...model.rowCount() do index = model.index(row, 0) text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt::CHECK_STATE_ROLE).toInt() if checkState == Qt::CHECKED checked = " +checked" end if selectionModel.isSelected(index) selected = " +selected" end Test.log("(#{row}) '#{text}'#{checked}#{selected}") end sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")) end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/itemviews/itemviews\"" set listViewName {{type='QListView' unnamed='1' visible='1'}} set listView [waitForObject $listViewName] set model [invoke $listView model] set selectionModel [invoke $listView selectionModel] for {set row 0} {$row < [invoke $model rowCount]} {incr row} { set index [invoke $model index $row 0] set text [toString [invoke [invoke $model data $index] toString]] set checked "" set selected "" set checkState [invoke [invoke $model data $index \ [enum Qt CheckStateRole]] toInt] if {$checkState == [enum Qt Checked]} { set checked " +checked" } if [invoke $selectionModel isSelected $index] { set selected " +selected" } test log "($row) '$text'$checked$selected" } sendEvent "QCloseEvent" [waitForObject ":Item Views_MainWindow"] }
请注意,模型中的所有数据都是通过使用QModelIndex访问的。模型索引有三个属性:一行、一列和一个父级。对于列表,只使用行——列始终为0;对于表格,使用行和列;对于树,则使用所有三个。
请注意,勾选状态是数据的一个属性,因此我们使用QAbstractItemModel.data
方法来访问它。(当我们不明确指定角色时,角色被认为是Qt.DisplayRole
,它通常包含项的文本。)QAbstractItemModel.data
方法返回一个QVariant,因此在使用之前我们必须将其转换为正确的类型。
在本子节和前一子节中,我们已经看到了如何遍历列表小部件和列表视图来检查每个项。在接下来的几个子节中,我们将为表格小部件和表格视图编写类似的测试。此外,我们还将展示如何使用数据填充表格小部件——同样的方法也可以用于填充列表或树小部件。填充模型的内容未显示,因为它与我们上面看到的内容非常相似——我们只需对每个要设置的值调用QAbstractItemModel.setData
方法,并提供适当的模型索引、角色和值。
如何测试QTableWidget中的项
在本节中,我们将查看两段示例代码。第一个示例显示如何设置表格的行数和列数,如何用项目填充表格——包括使项目可勾选和选中,以及如何隐藏行。第二个示例显示如何遍历表格中的每个项目(但跳过隐藏的行),并将项目文本和状态信息打印到Squish的日志中。(本节中显示的代码取自examples/qt/csvtable
示例的tst_iterating
测试套件。)
tableWidget = waitForObject({'type': 'QTableWidget', "unnamed":'1', 'visible':'1'}) tableWidget.setRowCount(4) tableWidget.setColumnCount(3) count = 0 for row in range(tableWidget.rowCount): for column in range(tableWidget.columnCount): tableItem = QTableWidgetItem("Item %d" % count) count += 1 if column == 2: tableItem.setCheckState(Qt.Unchecked) if row == 1 or row == 3: tableItem.setCheckState(Qt.Checked) tableWidget.setItem(row, column, tableItem) if count in (6, 10): tableItem.setSelected(True) tableWidget.setRowHidden(2, True)
var tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}"); tableWidget.setRowCount(4); tableWidget.setColumnCount(3); var count = 0; for (var row = 0; row < tableWidget.rowCount; ++row) { for (var column = 0; column < tableWidget.columnCount; ++column) { tableItem = new QTableWidgetItem("Item " + new String(count)); ++count; if (column == 2) { tableItem.setCheckState(Qt.Unchecked); if (row == 1 || row == 3) { tableItem.setCheckState(Qt.Checked); } } tableWidget.setItem(row, column, tableItem); if (count == 6 || count == 10) { tableItem.setSelected(true); } } } tableWidget.setRowHidden(2, true);
my $tableWidget = waitForObject("{type='QTableWidget' " . "unnamed='1' visible='1'}"); $tableWidget->setRowCount(4); $tableWidget->setColumnCount(3); my $count = 0; for (my $row = 0; $row < $tableWidget->rowCount; ++$row) { for (my $column = 0; $column < $tableWidget->columnCount; ++$column) { my $tableItem = new QTableWidgetItem("Item $count"); ++$count; if ($column == 2) { $tableItem->setCheckState(Qt::Unchecked); if ($row == 1 || $row == 3) { $tableItem->setCheckState(Qt::Checked); } } $tableWidget->setItem($row, $column, $tableItem); if ($count == 6 || $count == 10) { $tableItem->setSelected(1); } } } $tableWidget->setRowHidden(2, 1);
tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") tableWidget.setRowCount(4) tableWidget.setColumnCount(3) count = 0 0.upto(tableWidget.rowCount) do |row| 0.upto(tableWidget.columnCount) do |column| tableItem = QTableWidgetItem.new("Item #{count}") count += 1 if column == 2 tableItem.setCheckState(Qt::UNCHECKED) if row == 1 or row == 3 tableItem.setCheckState(Qt::CHECKED) end end tableWidget.setItem(row, column, tableItem) if count == 6 or count == 10 tableItem.setSelected(true) end end end tableWidget.setRowHidden(2, true)
set tableWidget [waitForObject {{type='QTableWidget' \ unnamed='1' visible='1'}}] invoke $tableWidget setRowCount 4 invoke $tableWidget setColumnCount 3 set count 0 for {set row 0} {$row < [property get $tableWidget rowCount]} \ {incr row} { for {set column 0} {$column < [property get $tableWidget \ columnCount]} {incr column} { set tableItem [construct QTableWidgetItem "Item $count"] incr count if {$column == 2} { invoke $tableItem setCheckState [enum Qt Unchecked] if {$row == 1 || $row == 3} { invoke $tableItem setCheckState \ [enum Qt Checked] } } invoke $tableWidget setItem $row $column $tableItem if {$count == 6 || $count == 10} { invoke $tableItem setSelected 1 } } } invoke $tableWidget setRowHidden 2 true
下方的截图显示了生成的表格
当然,这些示例中展示的方法可以用来设置表格小部件项的其他方面,比如它们的字体、背景颜色、文本对齐方式等。
无论我们是否使用上面显示的自定义测试代码设置了一个表格,还是有一个由其他方式(例如,由AUT加载数据文件)填充的数据表格,我们需要能够遍历表项,并检查它们的文本和其他属性。这正是下一个示例所展示的。
tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") for row in range(tableWidget.rowCount): if tableWidget.isRowHidden(row): test.log("Skipping hidden row %d" % row) continue for column in range(tableWidget.columnCount): tableItem = tableWidget.item(row, column) text = tableItem.text() checked = selected = "" if tableItem.checkState() == Qt.Checked: checked = " +checked" if tableItem.isSelected(): selected = " +selected" test.log("(%d, %d) '%s'%s%s" % (row, column, text, checked, selected))
tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}"); for (var row = 0; row < tableWidget.rowCount; ++row) { if (tableWidget.isRowHidden(row)) { test.log("Skipping hidden row " + String(row)); continue; } for (var column = 0; column < tableWidget.columnCount; ++column) { tableItem = tableWidget.item(row, column); var text = new String(tableItem.text()); var checked = ""; var selected = ""; if (tableItem.checkState() == Qt.Checked) { checked = " +checked"; } if (tableItem.isSelected()) { selected = " +selected"; } test.log("(" + String(row) + ", " + String(column) + ") '" + text + "' " + checked + selected); } }
$tableWidget = waitForObject( "{type='QTableWidget' " . "unnamed='1' visible='1'}" ); for ( my $row = 0 ; $row < $tableWidget->rowCount ; ++$row ) { if ( $tableWidget->isRowHidden($row) ) { test::log("Skipping hidden row $row"); next; } for ( my $column = 0 ; $column < $tableWidget->columnCount ; ++$column ) { my $tableItem = $tableWidget->item( $row, $column ); my $text = $tableItem->text(); my $checked = ""; my $selected = ""; if ( $tableItem->checkState() == Qt::Checked ) { $checked = " +checked"; } if ( $tableItem->isSelected() ) { $selected = " +selected"; } test::log("($row, $column) '$text'$checked$selected"); } }
tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") 0.upto(tableWidget.rowCount) do |row| if tableWidget.isRowHidden(row) Test.log("Skipping hidden row #{row}") next end 0.upto(tableWidget.columnCount) do |column| tableItem = tableWidget.item(row, column) if tableItem == nil next end text = tableItem.text() checked = selected = "" if tableItem.checkState() == Qt::CHECKED checked = " +checked" end if tableItem.isSelected() selected = " +selected" end Test.log("(%d, %d) '%s'%s%s" % [row, column, text, checked, selected]) end end
set tableWidget [waitForObject {{type='QTableWidget' \ unnamed='1' visible='1'}}] for {set row 0} {$row < [property get $tableWidget rowCount]} \ {incr row} { if {[invoke $tableWidget isRowHidden $row]} { test log "Skipping hidden row $row" continue } for {set column 0} {$column < [property get $tableWidget \ columnCount]} {incr column} { set tableItem [invoke $tableWidget item $row $column] set text [toString [invoke $tableItem text]] set checked "" set selected "" if {[invoke $tableItem checkState] == [enum Qt Checked]} { set checked " +checked" } if {[invoke $tableItem isSelected]} { set selected " +selected" } test log "($row, $column) '$text'$checked$selected" } }
上述代码生成的日志输出是
(0, 0) 'Item 0' (0, 1) 'Item 1' (0, 2) 'Item 2' (1, 0) 'Item 3' (1, 1) 'Item 4' (1, 2) 'Item 5' checked selected Skipping hidden row 2 (3, 0) 'Item 9' selected (3, 1) 'Item 10' (3, 2) 'Item 11' checked
如我们之前所述,可以使用相同的技术来测试其他属性,例如每个表格项的字体、背景颜色、文本对齐方式等。
测试整个表格的另一种有用方法是将其所有项目与一个格式为.tsv
(制表符分隔值格式)、.csv
(逗号分隔值格式)、.xls
或.xlsx
(Microsoft Excel电子表格格式)的数据文件进行比较。如何执行此操作的示例在如何测试表格控件和使用外部数据文件中给出。
如何测试QTableView中的项目(QAbstractItemModel和QItemSelectionModel)
与所有其他视图类一样,表格视图显示模型中持有的数据,而不是持有任何数据。因此,对表格视图显示的数据进行测试的关键是获取表格视图的模型,并在模型的-data上进行工作。以下示例(与前面展示的列表视图示例非常相似)显示了如何执行此操作。
import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/itemviews/itemviews"') tableViewName = "{type='QTableView' unnamed='1' visible='1'}" tableView = waitForObject(tableViewName) model = tableView.model() selectionModel = tableView.selectionModel() for row in range(model.rowCount()): for column in range(model.columnCount()): index = model.index(row, column) text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt.CheckStateRole).toInt() if checkState == Qt.Checked: checked = " +checked" if selectionModel.isSelected(index): selected = " +selected" test.log("(%d, %d) '%s'%s%s" % (row, column, text, checked, selected)) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow"))
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/itemviews/itemviews"'); var tableViewName = "{type='QTableView' unnamed='1' visible='1'}"; var tableView = waitForObject(tableViewName); var model = tableView.model(); var selectionModel = tableView.selectionModel(); for (var row = 0; row < model.rowCount(); ++row) { for (var column = 0; column < model.columnCount(); ++column) { var index = model.index(row, column); var text = model.data(index).toString(); var checked = ""; var selected = ""; var checkState = model.data(index, Qt.CheckStateRole).toInt(); if (checkState == Qt.Checked) { checked = " +checked"; } if (selectionModel.isSelected(index)) { selected = " +selected"; } test.log("(" + String(row) + ", " + String(column) + ") '" + text + "'" + checked + selected); } } sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/itemviews/itemviews\""); my $tableViewName = "{type='QTableView' unnamed='1' visible='1'}"; my $tableView = waitForObject($tableViewName); my $model = $tableView->model(); my $selectionModel = $tableView->selectionModel(); for ( my $row = 0 ; $row < $model->rowCount() ; ++$row ) { for ( my $column = 0 ; $column < $model->columnCount() ; ++$column ) { my $index = $model->index( $row, $column ); my $text = $model->data($index)->toString(); my $checked = ""; my $selected = ""; my $checkState = $model->data( $index, Qt::CheckStateRole )->toInt(); if ( $checkState == Qt::Checked ) { $checked = " +checked"; } if ( $selectionModel->isSelected($index) ) { $selected = " +selected"; } test::log("($row, $column) '$text'$checked$selected"); } } sendEvent( "QCloseEvent", waitForObject(":Item Views_MainWindow") ); }
# encoding: UTF-8 require 'squish' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/itemviews/itemviews\"") tableViewName = "{type='QTableView' unnamed='1' visible='1'}" tableView = waitForObject(tableViewName) model = tableView.model() selectionModel = tableView.selectionModel() for row in 0...model.rowCount() for column in 0...model.columnCount() index = model.index(row, column) text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt::CHECK_STATE_ROLE).toInt() if checkState == Qt::CHECKED checked = " +checked" end if selectionModel.isSelected(index) selected = " +selected" end Test.log("(#{row}, #{column}) '#{text}'#{checked}#{selected}") end end sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")) end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/itemviews/itemviews\"" set tableViewName {{type='QTableView' unnamed='1' visible='1'}} set tableView [waitForObject $tableViewName] set model [invoke $tableView model] set selectionModel [invoke $tableView selectionModel] for {set row 0} {$row < [invoke $model rowCount]} {incr row} { for {set column 0} {$column < [invoke $model columnCount]} \ {incr column} { set index [invoke $model index $row $column] set text [toString [invoke [invoke $model data $index] \ toString]] set checked "" set selected "" set checkState [invoke [invoke $model data $index \ [enum Qt CheckStateRole]] toInt] if {$checkState == [enum Qt Checked]} { set checked " +checked" } if [invoke $selectionModel isSelected $index] { set selected " +selected" } test log "($row, $column) '$text'$checked$selected" } } sendEvent "QCloseEvent" [waitForObject ":Item Views_MainWindow"] }
如果我们将其与前面展示的等效列表视图示例进行比较,很明显唯一的不同之处在于列表模型只有一个列——列0需要考虑,而表格模型有一个或多个列需要考虑。
如何测试QTreeWidget中的项目
树形控件(以及树视图中的模型)与列表或表格控件和视图测试起来相当不同。这是因为树有更复杂的底层结构。结构基本上是这样的:一系列行(顶级项),其中每一项可以有一个或多个列,并且每一项可以有自己的子项行。每个子项可以有一个或多个列,并且可以有自己的子项行,依此类推。
遍历树的最简单方法是通过使用递归过程(也就是说,一个调用自己的过程),从树的“不可见根项”开始,然后对每个项的子项进行操作,以及它们的子项,依此类推。下面的示例展示了如何做。(请注意,如果测试中有多个函数定义,Squish始终(且仅)调用名为main
的函数——这个函数可以调用其他函数,如所需。)
import os def checkAnItem(indent, item, root): if indent > -1: checked = selected = "" if item.checkState(0) == Qt.Checked: checked = " +checked" if item.isSelected(): selected = " +selected" test.log("|%s'%s'%s%s" % (" " * indent, item.text(0), checked, selected)) else: indent = -4 # Only show visible child items if item != root and item.isExpanded() or item == root: for row in range(item.childCount()): checkAnItem(indent + 4, item.child(row), root) def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/itemviews/itemviews"') treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}" treeWidget = waitForObject(treeWidgetName) root = treeWidget.invisibleRootItem() checkAnItem(-1, root, root) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow"))
function checkAnItem(indent, item, root) { if (indent > -1) { var checked = ""; var selected = ""; if (item.checkState(0) == Qt.Checked) { checked = " +checked"; } if (item.isSelected()) { selected = " +selected"; } var pad = ""; for (var i = 0; i < indent; ++i) { pad += " "; } test.log("|" + pad + "'" + item.text(0) + "'" + checked + selected); } else { indent = -4; } // Only show visible child items if (item != root && item.isExpanded() || item == root) { for (var row = 0; row < item.childCount(); ++row) { checkAnItem(indent + 4, item.child(row), root); } } } function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/itemviews/itemviews"'); var treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}"; var treeWidget = waitForObject(treeWidgetName); var root = treeWidget.invisibleRootItem(); checkAnItem(-1, root, root); sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")); }
sub checkAnItem { my ( $indent, $item, $root ) = @_; if ( $indent > -1 ) { my $checked = ""; my $selected = ""; if ( $item->checkState(0) == Qt::Checked ) { $checked = " +checked"; } if ( $item->isSelected() ) { $selected = " +selected"; } test::log( "|" . " " x $indent . "'" . $item->text(0) . "'" . $checked . $selected ); } else { $indent = -4; } # Only show visible child items if ( $item != $root && $item->isExpanded() || $item == $root ) { for ( my $row = 0 ; $row < $item->childCount() ; ++$row ) { checkAnItem( $indent + 4, $item->child($row), $root ); } } } sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/itemviews/itemviews\""); my $treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}"; my $treeWidget = waitForObject($treeWidgetName); my $root = $treeWidget->invisibleRootItem(); checkAnItem( -1, $root, $root ); sendEvent( "QCloseEvent", waitForObject(":Item Views_MainWindow") ); }
# encoding: UTF-8 require 'squish' include Squish def checkAnItem(indent, item, root) if indent > -1 checked = selected = "" if item.checkState(0) == Qt::CHECKED checked = " +checked" end if item.isSelected() selected = " +selected" end Test.log("|%s'#{item.text(0)}'#{checked}#{selected}" % (" " * indent)) else indent = -4 end # Only show visible child items if item != root and item.isExpanded() or item == root for row in 0...item.childCount() checkAnItem(indent + 4, item.child(row), root) end end end def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/itemviews/itemviews\"") treeWidgetName = "{type='QTreeWidget' unnamed='1' visible='1'}" treeWidget = waitForObject(treeWidgetName) root = treeWidget.invisibleRootItem() checkAnItem(-1, root, root) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")) end
proc checkAnItem {indent item root} { if {$indent > -1} { set checked "" set selected "" if {[invoke $item checkState 0] == [enum Qt Checked]} { set checked " +checked" } if [invoke $item isSelected] { set selected " +selected" } set text [toString [invoke $item text 0]] set pad [string repeat " " $indent] test log "|$pad'$text'$checked$selected" } else { set indent [expr -4] } # Only show visible child items if {$item != $root && [invoke $item isExpanded] || $item == $root} { for {set row 0} {$row < [invoke $item childCount]} {incr row} { checkAnItem [expr $indent + 4] [invoke $item child $row] \ $root } } } proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/itemviews/itemviews\"" set treeWidgetName {{type='QTreeWidget' unnamed='1' visible='1'}} set treeWidget [waitForObject $treeWidgetName] set root [invoke $treeWidget invisibleRootItem] checkAnItem -1 $root $root sendEvent "QCloseEvent" [waitForObject ":Item Views_MainWindow"] }
缩进仅用于在打印到Squish日志时显示树的结构,并且使用开头的|,因为Squish通常从日志消息的两端去除空白,我们不希望在这里这样做。例如
|'Green algae' | 'Chlorophytes' | 'Chlorophyceae' | 'Ulvophyceae' | 'Trebouxiophyceae' | 'Desmids & Charophytes' | 'Closteriaceae' +checked | 'Desmidiaceae' | 'Gonaozygaceae' +selected | 'Peniaceae' |'Bryophytes' |'Pteridophytes' | 'Club Mosses' | 'Ferns' |'Seed plants' | 'Cycads' +checked +selected | 'Ginkgo' | 'Conifers' | 'Gnetophytes' | 'Flowering Plants'
请注意,我们仅检查第一列的项目—if如果我们需要检查其他列的项目,我们必须引入一个循环来遍历列并使用列索引而不是简单地使用示例中的0(第一列)。
另一个需要注意的要点是'Bryophytes'条目实际上有三个子项('Liverworts'、'Hornworts'和'Mosses'),但这些项目没有显示出来,因为'Bryophytes'项目是折叠的(不显示其子项,并用+
表示它可展开,而其他项目有-
表示它们已展开)。在代码中,我们忽略不可见的子项——我们通过仅在当前项是树的根(即所有顶级项的概念性父项)时调用checkAnItem
函数,或者当前项不是根,但是展开的(这意味着它的子项在树中是可见的)来实现。当然,我们当然可以不跳过不可见的子项,只需通过删除checkAnItem
函数中的最后一个if
语句来实现。
请记住,即使项是可见的,它也可能对用户不可见——例如,如果该项不在树的可见区域内。然而,如果用户将其滚动到它,它将是可见的。
如何在 QTreeView 中测试项目(包括 QAbstractItemModel 和 QItemSelectionModel)
树视图使用树状结构模型,因此遍历所有模型项的最简单方法是使用递归过程,就像我们在前一节中为树小部件做的那样。以下是一个示例:
import os def checkAnItem(indent, index, treeView, model, selectionModel): if indent > -1 and index.isValid(): text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt.CheckStateRole).toInt() if checkState == Qt.Checked: checked = " +checked" if selectionModel.isSelected(index): selected = " +selected" test.log("|%s'%s'%s%s" % (" " * indent, text, checked, selected)) else: indent = -4 # Only show visible child items if (index.isValid() and treeView.isExpanded(index) or not index.isValid()): for row in range(model.rowCount(index)): checkAnItem(indent + 4, model.index(row, 0, index), treeView, model, selectionModel) def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/itemviews/itemviews"') treeViewName = "{type='QTreeView' unnamed='1' visible='1'}" treeView = waitForObject(treeViewName) model = treeView.model() selectionModel = treeView.selectionModel() checkAnItem(-1, QModelIndex(), treeView, model, selectionModel) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow"))
function checkAnItem(indent, index, treeView, model, selectionModel) { if (indent > -1 && index.isValid()) { var text = model.data(index).toString(); var checked = ""; var selected = ""; var checkState = model.data(index, Qt.CheckStateRole).toInt(); if (checkState == Qt.Checked) { checked = " +checked"; } if (selectionModel.isSelected(index)) { selected = " +selected"; } var pad = ""; for (var i = 0; i < indent; ++i) { pad += " "; } test.log("|" + pad + "'" + text + "'" + checked + selected); } else { indent = -4; } // Only show visible child items if (index.isValid() && treeView.isExpanded(index) || !index.isValid()) { for (var row = 0; row < model.rowCount(index); ++row) { checkAnItem(indent + 4, model.index(row, 0, index), treeView, model, selectionModel); } } } function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/itemviews/itemviews"'); var treeViewName = "{type='QTreeView' unnamed='1' visible='1'}"; var treeView = waitForObject(treeViewName); var model = treeView.model(); var selectionModel = treeView.selectionModel(); checkAnItem(-1, new QModelIndex(), treeView, model, selectionModel); sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")); }
sub checkAnItem { my ( $indent, $index, $treeView, $model, $selectionModel ) = @_; if ( $indent > -1 && $index->isValid() ) { my $text = $model->data($index)->toString(); my $checked = ""; my $selected = ""; my $checkState = $model->data( $index, Qt::CheckStateRole )->toInt(); if ( $checkState == Qt::Checked ) { $checked = " +checked"; } if ( $selectionModel->isSelected($index) ) { $selected = " +selected"; } test::log( "|" . " " x $indent . "'" . $text . "'" . $checked . $selected ); } else { $indent = -4; } # Only show visible child items if ( $index->isValid() && $treeView->isExpanded($index) || !$index->isValid() ) { for ( my $row = 0 ; $row < $model->rowCount($index) ; ++$row ) { checkAnItem( $indent + 4, $model->index( $row, 0, $index ), $treeView, $model, $selectionModel ); } } } sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/itemviews/itemviews\""); my $treeViewName = "{type='QTreeView' unnamed='1' visible='1'}"; my $treeView = waitForObject($treeViewName); my $model = $treeView->model(); my $selectionModel = $treeView->selectionModel(); checkAnItem( -1, new QModelIndex(), $treeView, $model, $selectionModel ); sendEvent( "QCloseEvent", waitForObject( ":Item Views_MainWindow" ) ); }
# encoding: UTF-8 require 'squish' include Squish def checkAnItem(indent, index, treeView, model, selectionModel) if indent > -1 and index.isValid() text = model.data(index).toString() checked = selected = "" checkState = model.data(index, Qt::CHECK_STATE_ROLE).toInt() if checkState == Qt::CHECKED checked = " +checked" end if selectionModel.isSelected(index) selected = " +selected" end Test.log("|%s'#{text}'#{checked}#{selected}" % (" " * indent)) else indent = -4 end # Only show visible child items if index.isValid() and treeView.isExpanded(index) or not index.isValid() for row in 0...model.rowCount(index) checkAnItem(indent + 4, model.index(row, 0, index), treeView, model, selectionModel) end end end def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/itemviews/itemviews\"") treeViewName = "{type='QTreeView' unnamed='1' visible='1'}" treeView = waitForObject(treeViewName) model = treeView.model() selectionModel = treeView.selectionModel() checkAnItem(-1, QModelIndex.new, treeView, model, selectionModel) sendEvent("QCloseEvent", waitForObject(":Item Views_MainWindow")) end
proc checkAnItem {indent index treeView model selectionModel} { if {$indent > -1 && [invoke $index isValid]} { set text [toString [invoke [invoke $model data $index] toString]] set checked "" set selected "" set checkState [invoke [invoke $model data $index \ [enum Qt CheckStateRole]] toInt] if {$checkState == [enum Qt Checked]} { set checked " +checked" } if [invoke $selectionModel isSelected $index] { set selected " +selected" } set pad [string repeat " " $indent] test log "|$pad'$text'$checked$selected" } else { set indent [expr -4] } # Only show visible child items if {[invoke $index isValid] && \ [invoke $treeView isExpanded $index] || \ ![invoke $index isValid]} { for {set row 0} {$row < [invoke $model rowCount $index]} \ {incr row} { checkAnItem [expr $indent + 4] [invoke $model index \ $row 0 $index] $treeView $model $selectionModel } } } proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/itemviews/itemviews\"" set treeViewName {{type='QTreeView' unnamed='1' visible='1'}} set treeView [waitForObject $treeViewName] set model [invoke $treeView model] set selectionModel [invoke $treeView selectionModel] checkAnItem -1 [construct QModelIndex] $treeView $model \ $selectionModel sendEvent "QCloseEvent" [waitForObject ":Item Views_MainWindow"] }
这里的代码结构和遍历树小部件项的代码几乎相同,只是这里我们使用模型索引来识别项。在模型中,“不可见根项”由一个无效的模型索引表示,即没有任何参数创建的模型索引。(上述 main 函数中的最后一条语句显示如何创建一个无效模型索引。)通过使用递归过程,我们可以确保无论树的深度如何,都可以遍历整个树。
就像之前所示的 QTreeWidget 示例一样,对于 QTreeView,我们跳过了已折叠(不可见)的子项。我们可以通过仅移除 checkAnItem
函数中的最后一个 if
语句,轻松地不跳过它们。
如何测试表格小部件并使用外部数据文件
在本节中,我们将了解如何测试下面的 csvtable
程序。这个程序使用 QTableWidget 来显示 .csv
(逗号分隔值)文件的内容,并提供一些基本功能来操作数据——插入和删除行、编辑单元格以及交换列。
注意:还可以导入测试数据文件,如 .tsv
(制表符分隔的值格式)、.csv
(逗号分隔的值格式)、.xls
或 .xlsx
(Microsoft Excel 工作表格式)。假设 .csv
和 .tsv
文件都使用 Unicode UTF-8 编码——与所有测试脚本相同的编码。
当我们在回顾测试时,我们将学习如何导入测试数据、操作数据,并将 QTableWidget
显示的内容与我们预期的内容进行比较。由于 csvtable
程序是一种主窗口样式应用程序,我们还将学习如何测试菜单选项和工具栏按钮是否按预期工作(并隐含地执行其底层操作)。此外,我们还将开发一些可能在多个不同测试中有用的通用函数。
此示例的源代码位于目录 SQUISHDIR/examples/qt/csvtable
中,测试套件位于其下的子目录中——例如,测试的 Python 版本位于目录 SQUISHDIR/examples/qt/csvtable/suite_py
中,测试的 JavaScript 版本位于 SQUISHDIR/examples/qt/csvtable/suite_js
中,等等。
注意:使用表格验证点来检查整个表格。请参阅 如何创建和使用表格验证。
我们将首先查看的测试非常简单,只包含四个可执行语句。这种简单性是通过将几乎所有功能集中在共享脚本中来实现的,以避免代码重复。以下是代码:
import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/csvtable/csvtable"') source(findFile("scripts", "common.py")) doFileOpen(findFile("testdata", "before.csv")) tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") compareTableWithDataFile(tableWidget, "before.csv")
function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/csvtable/csvtable"'); source(findFile("scripts", "common.js")); doFileOpen("suite_js/shared/testdata/before.csv"); tableWidget = waitForObject("{type='QTableWidget' unnamed='1' " + "visible='1'}"); compareTableWithDataFile(tableWidget, "before.csv"); }
sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/csvtable/csvtable\""); source(findFile("scripts", "common.pl")); doFileOpen(findFile("testdata", "before.csv")); my $tableWidget = waitForObject("{type='QTableWidget' " . "unnamed='1' visible='1'}"); compareTableWithDataFile($tableWidget, "before.csv"); }
# encoding: UTF-8 require 'squish' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/csvtable/csvtable\"") require findFile("scripts", "common.rb") doFileOpen(findFile("testdata", "before.csv")) tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}") compareTableWithDataFile(tableWidget, "before.csv") end
proc main {} { startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/csvtable/csvtable\"" source [findFile "scripts" "common.tcl"] doFileOpen [findFile "testdata" "before.csv"] set tableWidget [waitForObject {{type='QTableWidget' \ unnamed='1' visible='1'}}] compareTableWithDataFile $tableWidget "before.csv" }
我们首先加载包含常见功能的脚本,就像我们在上一节中所做的那样。然后我们调用自定义函数 doFileOpen
,告诉程序打开指定的文件——这将通过用户界面完成,正如我们将看到的。接下来我们使用Object waitForObject(objectOrName) 函数来获取表格小部件的引用,并最后检查表格小部件的内容与测试套件测试数据中的数据文件内容相匹配。请注意,csvtable
程序和 Squish 都使用它们自己的完全独立代码来加载和解析数据文件。有关如何将测试数据导入 Squish 的信息,请参阅如何创建和使用共享数据和共享脚本。
现在我们将查看上述测试中使用的自定义函数。
def doFileOpen(path_and_filename): activateItem(waitForObjectItem(names.cSV_Table_QMenuBar, "File")) activateItem(waitForObjectItem(names.cSV_Table_File_QMenu, "Open...")) waitForObject(names.fileNameEdit_QLineEdit) fn = os.path.join(squishinfo.testCase, path_and_filename) type(names.fileNameEdit_QLineEdit, fn) clickButton(names.cSV_Table_Choose_File_Open_QPushButton) def compareTableWithDataFile(tableWidget, filename): for row, record in enumerate(testData.dataset(filename)): for column, name in enumerate(testData.fieldNames(record)): tableItem = tableWidget.item(row, column) test.compare(testData.field(record, name), tableItem.text())
function doFileOpen(path_and_filename) { activateItem(waitForObjectItem(names.cSVTableAfterCsvQMenuBar, "File")); activateItem(waitForObjectItem(names.cSVTableAfterCsvFileQMenu, "Open...")); waitForObject(names.fileNameEditQLineEdit); components = path_and_filename.split("/"); for (var i = 0; i < components.length; ++i) { type(names.fileNameEditQLineEdit, components[i]); waitForObject(names.fileNameEditQLineEdit); type(names.fileNameEditQLineEdit, "<Return>"); } } function chooseMenuOptionByKey(menuTitle, menuKey, optionKey) { windowName = "{type='MainWindow' unnamed='1' visible='1' " + "windowTitle?='CSV Table*'}"; waitForObject(windowName); type(windowName, "<Alt+" + menuKey + ">"); menuName = "{title='" + menuTitle + "' type='QMenu' unnamed='1' " + "visible='1'}"; waitForObject(menuName); type(menuName, optionKey); } function compareTableWithDataFile(tableWidget, filename) { records = testData.dataset(filename); for (var row = 0; row < records.length; ++row) { columnNames = testData.fieldNames(records[row]); for (var column = 0; column < columnNames.length; ++column) { tableItem = tableWidget.item(row, column); test.compare(testData.field(records[row], column), tableItem.text()); } } }
sub doFileOpen { my $path_and_filename = shift(@_); activateItem(waitForObjectItem( $Names::csv_table_after_csv_qmenubar, "File" )); activateItem(waitForObjectItem( $Names::csv_table_after_csv_file_qmenu, "Open..." )); my $fn = File::Spec->catfile(squishinfo->testCase, $path_and_filename); type($Names::filenameedit_qlineedit, $fn ); clickButton($Names::csv_table_choose_file_open_qpushbutton); } sub compareTableWithDataFile { my ( $tableWidget, $filename ) = @_; my @records = testData::dataset($filename); for ( my $row = 0 ; $row < scalar(@records) ; $row++ ) { my @columnNames = testData::fieldNames( $records[$row] ); for ( my $column = 0 ; $column < scalar(@columnNames) ; $column++ ) { my $tableItem = $tableWidget->item( $row, $column ); test::compare( $tableItem->text(), testData::field( $records[$row], $column ) ); } } }
def doFileOpen(path_and_filename) activateItem(waitForObjectItem(Names::CSV_Table_QMenuBar, "File")) activateItem(waitForObjectItem(Names::CSV_Table_File_QMenu, "Open...")) fn = File.join(Squishinfo.testCase, path_and_filename) type(Names::FileNameEdit_QLineEdit, fn); clickButton(Names::CSV_Table_Choose_File_Open_QPushButton) end def compareTableWithDataFile(tableWidget, filename) TestData.dataset(filename).each_with_index do |record, row| for column in 0...TestData.fieldNames(record).length tableItem = tableWidget.item(row, column) Test.compare(TestData.field(record, column), tableItem.text()) end end end
proc doFileOpen {path_and_filename} { invoke activateItem [waitForObjectItem $names::CSV_Table_QMenuBar "File"] invoke activateItem [waitForObjectItem $names::CSV_Table_File_QMenu "Open..."] set fn [file join [squishinfo testCase] $path_and_filename] invoke type $names::fileNameEdit_QLineEdit $fn invoke clickButton $names::CSV_Table_Choose_File_Open_QPushButton } proc chooseMenuOptionByKey {menuTitle menuKey optionKey} { set windowName "{type='MainWindow' unnamed='1' visible='1' \ windowTitle?='CSV Table*'}" waitForObject $windowName invoke type $windowName "<Alt+$menuKey>" set menuName "{title='$menuTitle' type='QMenu' unnamed='1' \ visible='1'}" waitForObject $menuName invoke type $menuName $optionKey } proc compareTableWithDataFile {tableWidget filename} { set data [testData dataset $filename] for {set row 0} {$row < [llength $data]} {incr row} { set columnNames [testData fieldNames [lindex $data $row]] for {set column 0} {$column < [llength $columnNames]} {incr column} { set tableItem [invoke $tableWidget item $row $column] test compare [testData field [lindex $data $row] $column] \ [invoke $tableItem text] } } }
传入参数的文件,首先通过用户界面来打开doFileOpen
函数。这是通过使用自定义函数chooseMenuOptionByKey
来实现的。有关 chooseMenuOptionByKey
函数的一个要注意的点是其使用通配符匹配 windowTitle
属性(使用 ?=
而不是使用 =
进行等式测试;有关更多信息,请参阅优化对象识别)。这对于显示当前文件名或其他可能变化的文本的窗口特别有用。此函数通过模拟用户点击Alt+k(其中 k 是一个字符,例如 "F" 表示文件菜单)然后是所需的动作对应的字符(例如,"o" 表示 "打开")来实现。一旦文件打开对话框弹出,对于我们要选择的路径中的每个组件和文件,doFileOpen
函数将输入一个组件后跟 Return,这将导致打开文件。
文件打开后,程序预计将加载文件的数据。我们通过比较表格小部件中显示的数据和数据文件本身来检查数据是否正确加载。这个比较是通过自定义函数 compareTableWithDataFile
来进行的。此函数使用 Squish 的 Dataset testData.dataset(filename) 函数加载数据,以便可以通过 Squish API 访问它。我们期望表中的每一项都与数据中的相应项匹配,并使用Boolean test.compare(value1, value2) 函数来检查这一点。
现在我们知道了如何将表格的数据与文件中的数据进行比较,我们可以进行一些更雄心勃勃的测试。我们将加载 before.csv
文件,删除第一行、最后一行和中间一行,在开始和在中间插入一行新行,并在末尾追加一行新行。然后我们将交换三对列。最后,数据应与 after.csv
文件相匹配。
我们不是编写执行所有这些操作的代码,而是记录一个打开文件并执行所有删除、插入和列交换的测试脚本。然后我们可以编辑记录的测试脚本以添加几行代码,将这些实际结果与预期结果进行比较。下面展示了添加的行及其上下文。
# Added by hand source(findFile("scripts", "common.py")) tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") compareTableWithDataFile(tableWidget, "after.csv") # End of added by hand waitForObject(names.cSV_Table_before_csv_File_QTableWidget) sendEvent("QCloseEvent", waitForObject(names.cSV_Table_MainWindow)) waitForObject("{type='QPushButton' unnamed='1' text='No'}") clickButton("{type='QPushButton' unnamed='1' text='No'}")
// Added by hand source(findFile("scripts", "common.js")); tableWidget = waitForObject({"type":'QTableWidget', "unnamed":'1', "visible":'1'}); compareTableWithDataFile(tableWidget, "after.csv"); // End of added by hand sendEvent("QCloseEvent", waitForObject(names.cSVTableMainWindow)); waitForObject({"type":'QPushButton', "unnamed":'1', "text":'No'}); clickButton({"type":'QPushButton', "unnamed":'1', "text":'No'});
# Added by hand source(findFile("scripts", "common.pl")); $tableWidget = waitForObject("{type='QTableWidget' " . "unnamed='1' visible='1'}"); compareTableWithDataFile($tableWidget, "after.csv"); sendEvent( "QCloseEvent", waitForObject($Names::csv_table_mainwindow) ); waitForObject("{type='QPushButton' unnamed='1' text='No'}"); clickButton("{type='QPushButton' unnamed='1' text='No'}");
require findFile("scripts", "common.rb") # Added by hand tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") compareTableWithDataFile(tableWidget, "after.csv") sendEvent("QCloseEvent", waitForObject(Names::CSV_Table_MainWindow)) waitForObject("{type='QPushButton' unnamed='1' text='No'}") clickButton("{type='QPushButton' unnamed='1' text='No'}")
# Added by hand source [findFile "scripts" "common.tcl"] set tableWidget [waitForObject {{type='QTableWidget' \ unnamed='1' visible='1'}}] compareTableWithDataFile $tableWidget "after.csv" sendEvent QCloseEvent [waitForObject $names::CSV_Table_MainWindow] waitForObject "{type='QPushButton' unnamed='1' text='No'}" invoke clickButton "{type='QPushButton' unnamed='1' text='No'}"
如您所看到的,添加的行不是在记录的测试脚本末尾插入,而是在程序终止之前插入。
当然,我们可以执行其他测试,例如检查表格的一些属性。以下是一个检查行和列计数是我们所期望的示例
tableWidget = waitForObject("{type='QTableWidget' " + "unnamed='1' visible='1'}") test.verify(tableWidget.rowCount == 12) test.verify(tableWidget.columnCount == 5)
tableWidget = waitForObject({"type":'QTableWidget', "unnamed":'1', "visible":'1'}); test.verify(tableWidget.rowCount == 12); test.verify(tableWidget.columnCount == 5);
my $tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}"); test::verify($tableWidget->rowCount == 12); test::verify($tableWidget->columnCount == 5);
tableWidget = waitForObject("{type='QTableWidget' unnamed='1' visible='1'}") Test.verify(tableWidget.rowCount == 12) Test.verify(tableWidget.columnCount == 5)
set tableWidget [waitForObject {{type='QTableWidget' \ unnamed='1' visible='1'}}] test compare [property get $tableWidget rowCount] 12 test compare [property get $tableWidget columnCount] 5
本代码片段假定我们已经使用了source(filename)函数(或Ruby中的require
)来使自定义函数可用。
注意:尽管在Tcl中可用Boolean test.verify(condition)方法,但如我们所示,使用Boolean test.compare(value1, value2)方法通常更为方便。
此示例展示了将录制与手动编辑相结合的强大功能。如果在以后日期程序中添加了新功能,我们可以以多种方式对其进行测试。最简单的方法是添加另一个测试脚本,进行录制,然后添加三条与预期数据比较表所需的线。另一种方法是在一个临时测试中录制对新功能的使用,然后将录制内容粘贴到现有测试的合适位置,最后在测试结束时更改要比较的文件,以包含所有原始数据的更改以及新功能使用导致的更改。或者我们可以使用squishide
在现有测试的中间录制代码片段。
如何测试QAction、QMenu和QMenuBar
如果我们想检查菜单项的属性,可以使用squishide
并插入验证点,或者我们可以直接在代码中编写验证。在此,我们将展示如何直接在代码中编写它们。
QMenu(以及QWidget)有一个QAction对象的列表。我们可以使用QList API检索此列表,并遍历其操作,对于每个操作,我们可以查询或设置其属性。首先,我们将查看访问操作属性的示例,然后我们将看到示例所依赖的用于实现自定义getAction
函数的实现。
editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu") removeAction = getAction(editMenu, "&Remove Row") test.verify(not removeAction.enabled) test.verify(not removeAction.checked) insertRowAction = getAction(editMenu, "&Insert Row") test.verify(insertRowAction.enabled) test.verify(not insertRowAction.checked)
var editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu"); var removeAction = getAction(editMenu, "&Remove Row"); test.verify(!removeAction.enabled); test.verify(!removeAction.checked); var insertRowAction = getAction(editMenu, "&Insert Row"); test.verify(insertRowAction.enabled); test.verify(!insertRowAction.checked);
my $editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu"); my $removeAction = getAction($editMenu, "&Remove Row"); test::verify(!$removeAction->enabled); test::verify(!$removeAction->checked); my $insertRowAction = getAction($editMenu, "&Insert Row"); test::verify($insertRowAction->enabled); test::verify(!$insertRowAction->checked);
editMenu = waitForObject(":CSV Table - Unnamed.Edit_QMenu") removeAction = getAction(editMenu, "&Remove Row") Test.verify(!removeAction.enabled) Test.verify(!removeAction.checked) insertRowAction = getAction(editMenu, "&Insert Row") Test.verify(insertRowAction.enabled) Test.verify(!insertRowAction.checked)
set menu [waitForObject ":CSV Table.Edit_QMenu"] set removeAction [getAction $menu "Disabled"] test compare [property get $removeAction enabled] 0 test compare [property get $removeAction checked] 0 set insertRowAction [getAction $menu "&Insert Row"] test compare [property get $insertRowAction enabled] 1 test compare [property get $insertRowAction checked] 0
在这里,我们获取应用程序的编辑菜单的引用,并检查删除行操作是否禁用且未选中,以及插入行操作是否启用且未选中。
通常情况下,我们更喜欢在Tcl中使用Boolean test.compare(value1, value2)函数而不是Boolean test.verify(condition)函数。
def getAction(widget, text): actions = widget.actions() for i in range(actions.count()): action = actions.at(i) if action.text == text: return action
function getAction(widget, text) { var actions = widget.actions(); for (var i = 0; i < actions.count(); ++i) { var action = actions.at(i); if (action.text == text) { return action; } } }
sub getAction { my ($widget, $text) = @_; my $actions = $widget->actions(); for (my $i = 0; $i < $actions->count(); ++$i) { my $action = $actions->at($i); if ($action->text eq $text) { return $action; } } }
def getAction(widget, text) actions = widget.actions() for i in 0...actions.count() action = actions.at(i) if action.text == text return action end end end
proc getAction {widget text} { set actions [invoke $widget actions] for {set index 0} {$index < [invoke $actions count]} \ {incr index} { set action [invoke $actions at $index] set action_text [toString [property get $action text]] if {[string equal $action_text $text]} { return $action } } }
此小程序检索给定小部件(或菜单)的动作列表,并遍历它们,直到找到匹配文本的动作。然后它返回相应的操作(如果找不到匹配项,则返回null)。
如何测试图形视图、图形场景和图形项
Qt 4.2通过QGraphicsView、QGraphicsScene和QGraphicsItem类引入了图形/视图架构,还添加了许多QGraphicsItem子类。Qt 4.4添加了几个额外类,在Qt 4.6中又添加了另外几个。Squish为测试使用此架构的应用程序提供全面的支持。
在本节中,我们将测试一个简单的示例应用程序(examples/qt/shapes
),该应用程序将其主要窗口的中心区域用作图形视图。该场景包括标准小部件,并提供添加更多QGraphicsItem的方法。截图所示的Shapes应用程序已添加并移动了几个图形项。
形状应用程序的按钮、标签、旋钮和液晶数字小部件都是标准的 QWidget
子类,它们被作为 QGraphicsProxyWidget 添加到视图中。用户可以点击相应的按钮添加矩形框(《a href="https://doc.qt.ac.cn/qt-5/qgraphicsrectitem.html" translate="no">QGraphicsRectItem)、多边形(这些是应用程序特有的自定义 RegularPolygonItem
元素——它们总是从三角形开始,但有一个上下文菜单以将它们更改为正方形或再次变回三角形),以及文本元素(《a href="https://doc.qt.ac.cn/qt-5/qgraphicstextitem.html" translate="no">QGraphicsTextItem)。视图已经启用了橡皮筋选择功能,以便更容易选择多个项目(当然,不包括小部件)。用户可以通过拖动项目来移动它们,通过选择它们并点击删除按钮来删除它们,并通过对旋钮操作来更改它们的 z 层次。
在本节中,我们将执行以下简单测试场景,以尝试各种形状应用程序的功能,并展示如何进行 Qt 的图形/视图架构测试。
- 启动时,请验证添加矩形框、添加多边形、添加文本和退出按钮已被启用,而删除按钮和 Z 旋钮被禁用。
- 添加两个矩形框,并验证第二个的 x 和 坐标比第一个多 5 像素,且第二个的 z 值比第一个多 1。
- 添加一个多边形,并确认它是一个三角形,即多边形恰好有三个点。
- 右键点击三角形,然后选择上下文菜单的 Square 选项;然后确认它已变为正方形,即多边形恰好有四个点。
- 添加一个文本项目,并确认输入对话框中输入的文本与文本项显示的文本匹配。
- 确认计数 LCD 显示 4 个项目,且删除按钮和 Z 旋钮被启用。
- 使用橡皮筋选择选择所有项目,即双击背景,然后点击并拖动直到所有项目都被选中,然后将它们拖到中间。现在使用橡皮筋选择只选择两个矩形框,然后点击删除,然后点击是。验证计数现在只显示 2 个项目,且删除按钮和 Z 旋钮被禁用。
- 退出应用程序。
我们可以使用 squishide
自动化测试,如下所示。创建一个新的测试套件和一个新的测试用例(例如,名称为 suite_py
的测试套件——或者更合理的名称,以及名为 tst_everything
的测试用例)。现在按照测试场景中的所有步骤进行操作——但 不要 为验证而担心!最终结果应该是一个大约 35 行的 Python 交互记录,以及其他脚本语言稍微多一点。
下一步是包括验证。我们可以直接在代码中这样做,或者我们可以使用 squishide
。要使用 squishide
,在需要验证的每个地方插入断点,然后运行脚本。当 squishide
停在断点时,你可以插入验证。无论这是使用 squishide
还是通过手工操作,结果都应相同。(另一种方法是,记录测试时插入验证——为自己的测试选择您选择的任何方法。)
对于这个例子,我们手动通过在记录的测试脚本的四个不同地方添加代码行来插入验证。我们在应用程序启动时就开始了验证,验证所有按钮都被启用——除了删除按钮——以及 Z 旋钮被禁用。这是我们为了实现这一点插入的代码:
test.verify(waitForObject(names.add_Box_QPushButton).enabled) test.verify(waitForObject(names.add_Polygon_QPushButton).enabled) test.verify(waitForObject(names.add_Text_QPushButton).enabled) test.verify(waitForObject(names.quit_QPushButton).enabled) test.verify(not waitForObjectExists(names.delete_QPushButton).enabled) test.verify(not waitForObjectExists(names.o_QSpinBox).enabled)
test.verify(waitForObject(names.addBoxQPushButton).enabled); test.verify(waitForObject(names.addPolygonQPushButton).enabled); test.verify(waitForObject(names.addTextQPushButton).enabled); test.verify(waitForObject(names.quitQPushButton).enabled); test.verify(!waitForObjectExists(names.deleteQPushButton).enabled); test.verify(!waitForObjectExists(names.qSpinBox).enabled);
test::verify(waitForObject($Names::add_box_qpushbutton)->enabled); test::verify(waitForObject($Names::add_polygon_qpushbutton)->enabled); test::verify(waitForObject($Names::add_text_qpushbutton)->enabled); test::verify(waitForObject($Names::quit_qpushbutton)->enabled); test::verify(!waitForObjectExists($Names::delete_qpushbutton)->enabled); test::verify(!waitForObjectExists($Names::o_qspinbox)->enabled);
Test.verify(waitForObject(Names::Add_Box_QPushButton).enabled) Test.verify(waitForObject(Names::Add_Polygon_QPushButton).enabled) Test.verify(waitForObject(Names::Add_Text_QPushButton).enabled) Test.verify(waitForObject(Names::Quit_QPushButton).enabled) Test.verify(!waitForObjectExists(Names::Delete_QPushButton).enabled) Test.verify(!waitForObjectExists(Names::O_QSpinBox).enabled)
test verify [property get [waitForObject $names::Add_Box_QPushButton] enabled] test verify [property get [waitForObject $names::Add_Polygon_QPushButton] enabled] test verify [property get [waitForObject $names::Add_Text_QPushButton] enabled] test verify [property get [waitForObject $names::Quit_QPushButton] enabled] test compare [property get [waitForObjectExists $names::Delete_QPushButton] enabled] 0 test compare [property get [waitForObjectExists $names::QSpinBox] enabled] 0
对于我们期望启用的对象,我们使用Object waitForObject(objectOrName)函数,而对于我们期望禁用的对象,我们必须使用Object findObject(objectName)函数代替。在所有情况下,我们都检索了对象并测试了其enabled
属性。
在添加两个矩形和一个多边形之后,我们插入了额外的代码来检查第二个矩形是否从中矩形正确偏移,并且多边形是一个三角形(即,有三个点)。
rectItem1 = waitForObject(names.o_QGraphicsRectItem) rectItem2 = waitForObject(names.o_QGraphicsRectItem_2) test.verify(rectItem1.rect.x + 5 == rectItem2.rect.x) test.verify(rectItem1.rect.y + 5 == rectItem2.rect.y) test.verify(rectItem1.zValue < rectItem2.zValue) polygonItem = waitForObject(names.o_QGraphicsPolygonItem) test.verify(polygonItem.polygon.count() == 3)
var rectItem1 = waitForObject(names.qGraphicsRectItem); var rectItem2 = waitForObject(names.qGraphicsRectItem2); test.verify(rectItem1.rect.x + 5 == rectItem2.rect.x); test.verify(rectItem1.rect.y + 5 == rectItem2.rect.y); test.verify(rectItem1.zValue < rectItem2.zValue); var polygonItem = waitForObject(names.qGraphicsPolygonItem) test.verify(polygonItem.polygon.count() == 3);
my $rectItem1 = waitForObject($Names::o_qgraphicsrectitem); my $rectItem2 = waitForObject($Names::o_qgraphicsrectitem_2); test::verify($rectItem1->rect->x + 5 eq $rectItem2->rect->x); test::verify($rectItem1->rect->y + 5 eq $rectItem2->rect->y); test::verify($rectItem1->zValue lt $rectItem2->zValue); my $polygonItem = waitForObject($Names::o_qgraphicspolygonitem); test::verify($polygonItem->polygon->count() == 3);
rectItem1 = waitForObject(Names::O_QGraphicsRectItem) rectItem2 = waitForObject(Names::O_QGraphicsRectItem_2) Test.verify(rectItem1.rect.x + 5 == rectItem2.rect.x) Test.verify(rectItem1.rect.y + 5 == rectItem2.rect.y) Test.verify(rectItem1.zValue < rectItem2.zValue) polygonItem = waitForObject(Names::O_QGraphicsPolygonItem) Test.verify(polygonItem.polygon.count() == 3)
set rectItem1 [waitForObject $names::QGraphicsRectItem] set rectItem2 [waitForObject $names::QGraphicsRectItem_2] set rectItem1X [property get [property get $rectItem1 rect] x] set rectItem1Y [property get [property get $rectItem1 rect] y] set rectItem2X [property get [property get $rectItem2 rect] x] set rectItem2Y [property get [property get $rectItem2 rect] y] test compare $rectItem2X [expr $rectItem1X + 5] test compare $rectItem2Y [expr $rectItem1Y + 5] test verify [expr [property get $rectItem1 zValue] < [property get $rectItem2 zValue]] set polygonItem [waitForObject $names::QGraphicsPolygonItem] test compare [invoke [property get $polygonItem polygon] count] 3
在这里,我们等待每个矩形创建完成,然后验证第二个矩形的x和y坐标比第一个矩形大5像素,并且第二个矩形的z值更高。我们还检查多边形项目的多边形有三个点。
记录的代码现在右击多边形项目,使用其上下文菜单将其更改为正方形。它还添加了一个新的文本项,内容为“一些文本”。因此,我们手动添加了第三段代码来检查一切是否正常。
test.verify(polygonItem.polygon.count() == 4) textItem = waitForObject(names.o_QGraphicsTextItem) test.verify(textItem.toPlainText() == "Some Text") countLCD = waitForObject(names.o_QLCDNumber) test.verify(countLCD.intValue == 4) test.verify(waitForObject(names.delete_QPushButton).enabled) test.verify(waitForObject(names.o_QSpinBox).enabled)
test.verify(polygonItem.polygon.count() == 4); var textItem = waitForObject(names.qGraphicsTextItem); test.verify(textItem.toPlainText() == "Some Text"); var countLCD = waitForObject(names.qLCDNumber); test.verify(countLCD.intValue == 4); test.verify(waitForObject(names.deleteQPushButton).enabled); test.verify(waitForObject(names.qSpinBox).enabled);
test::verify($polygonItem->polygon->count() == 4); my $textItem = waitForObject($Names::o_qgraphicstextitem); test::verify($textItem->toPlainText() eq "Some Text"); my $countLCD = waitForObject($Names::o_qlcdnumber); test::verify($countLCD->intValue == 4); test::verify(waitForObject($Names::delete_qpushbutton)->enabled); test::verify(waitForObject($Names::o_qspinbox)->enabled);
Test.verify(polygonItem.polygon.count() == 4) textItem = waitForObject(Names::O_QGraphicsTextItem) Test.verify(textItem.toPlainText() == "Some Text") countLCD = waitForObject(Names::O_QLCDNumber) Test.verify(countLCD.intValue == 4) Test.verify(waitForObject(Names::Delete_QPushButton).enabled) Test.verify(waitForObject(Names::O_QSpinBox).enabled)
test compare [invoke [property get $polygonItem polygon] count] 4 set textItem [waitForObject $names::QGraphicsTextItem] test compare [invoke $textItem toPlainText] "Some Text" set countLCD [waitForObject $names::QLCDNumber] test compare [invoke $countLCD intValue] 4 test verify [property get [waitForObject $names::Delete_QPushButton] enabled] test verify [property get [waitForObject $names::QSpinBox] enabled]
我们首先验证多边形项目现在有四个点(即现在是一个正方形)。然后我们检索文本项并验证其文本是否是我们输入的。使用QLCDNumber
来显示项目数量,因此我们检查它显示正确的数字。最后,我们验证删除按钮和Z微调框都处于启用状态。
在删除几个项目并单击视图(以确保没有项目被选中)之后,我们插入最终的验证代码。
countLCD = waitForObject(names.o_QLCDNumber) test.verify(countLCD.intValue == 2) test.verify(not waitForObjectExists(names.delete_QPushButton).enabled) test.verify(not waitForObjectExists(names.o_QSpinBox).enabled)
var countLCD = waitForObject(names.qLCDNumber); test.verify(countLCD.intValue == 2); test.verify(!waitForObjectExists(names.deleteQPushButton).enabled); test.verify(!waitForObjectExists(names.qSpinBox).enabled);
$countLCD = waitForObject($Names::o_qlcdnumber); test::verify($countLCD->intValue == 2); test::verify(!waitForObjectExists($Names::delete_qpushbutton)->enabled); test::verify(!waitForObjectExists($Names::o_qspinbox)->enabled);
删除了两个项目后,应该只剩下两个,因此我们验证QLCDNumber
是否正确反映了这一点。另外,在没有选择项目的情况下,删除按钮和Z微调框都应该被禁用,所以我们再次验证这一点。
这些验证代码直接插入到记录的脚本的最后一行之前(用于单击退出按钮)。
整个脚本,包含记录和手动添加的部分,位于examples/qt/shapes/suite_py/tst_everything/test.py
(或JavaScript的suite_js/tst_everything/test.js
等)。虽然我们手动添加了验证,但我们也可以通过插入断点,导航到感兴趣的控件或项,单击我们想要验证的属性,然后插入脚本文档化的验证点来执行这一操作。或者我们可以在录制时插入验证,通过单击控制栏窗口的任何工具栏按钮来插入验证点。通常最好使用脚本化的验证,因为如果在以后想要更改它们,它们最容易手动编辑。
测试图形/视图场景与测试任何其他Qt控件或项目一样简单。Squish为每个图形项提供合理的符号名称,因此识别它们并不困难——当然,我们总是可以插入一个断点并使用Spy来识别感兴趣的所有项,并将它们添加到对象映射中。
有关测试图形/视图项的更多信息,请参阅QObject castToQObject(object)函数。
如何在Qt应用程序中测试非Qt控件
Squish for Qt旨在支持自动化Qt应用程序中Qt控件的测试。然而,在某些平台上,Qt应用程序使用Qt和本地控件的混合构建——例如,在Windows上,Qt应用程序可能使用本地的Windows对话框和嵌入式ActiveX控件,除了Qt控件。
幸运的是,Squish 支持记录和重放键盘和鼠标操作,针对所有原生 Windows 控件。此外,使用 Squish 间谍工具可以检查标准 Windows 控件的属性,并在测试脚本中插入对这些控件的验证,以及访问它们的属性。注意,还有针对 Windows 的特定 Squish 版本,该版本与标准 Windows 应用程序(如使用 MFC 或 .NET 技术创建的应用程序)兼容。
如何对 Qt 进行自动压力测试
本节解释了如何使用 Squish 为您的应用程序实现完全自动化的压力测试。
本节所实施的压力测试称为《猴测试》。这个名字来自一个想法,即如果你有一屋子的猴子和打字机,并且几乎无限的时间和替换,它们最终会敲打出所有的文学巨著。
注意:目前 Squish 通过基于 JavaScript 的测试套件为 Qt 工具箱提供猴测试。所有猴测试代码都是用 JavaScript 编写的,尽管没有理由不能用 Squish 支持的任何脚本语言编写。此外,鉴于 Squish 对工具包 API 的出色访问,应该可以创建任何 Squish 支持的工具包的自动测试——例如,通过调整 JavaScript 猴测试(并且如果需要,还可能将其转换为另一种脚本语言)。
在压力测试中,有《智能猴子》和《愚蠢猴子》。智能猴子对于加载和压力测试非常有价值;它们会发现大量的错误,但是它们的开发成本也很高。它们通常还需要一定量的关于您的应用程序的知识,它是什么,以及它能做什么,不能做什么。另一方面,愚蠢猴子开发成本较低,并且能够进行一些基本的测试——但是它们会发现 fewer bugs。然而,愚蠢猴子发现的错误通常是不稳定和崩溃,也就是说,您最想发现的错误!愚蠢猴子也不需要了解您应用程序的太多(如果有什么的话),因此它们很容易创建。
注意:尽管猴测试是对您的测试环境的重要补充,但它绝对不应仅用于测试,也不应取代任何形式的验收测试。
本教程中使用的猴测试使用了一个不是完全愚蠢的猴子。这意味着尽管猴子不需要了解您的应用程序,但它确实知道应用中的一些一般性内容,例如按钮、输入字段和复选框是什么,以及如何与其交互。因此,猴子不会随机地随机点击您的 GUI,而是会选择一些用户可访问的控件并与之交互。
启动猴测试
使用猴测试对您的自动测试进行压力测试最简单的方法是修改 examples/suite_monkeytest_js
测试套件的 tst_runmonkey
测试用例,如下所示
- 确保要测试的应用程序已在 squishserver 中注册。如果不是这样,请参阅 AUTs 和设置 部分了解如何执行此操作。
- 打开测试套件
examples/suite_monkeytest_js
。此示例随 Squish 一起提供。 - 转到
tst_runmonkey
测试用例并打开test.js
测试脚本。转到脚本中的main
函数的第一行代码(以var monkey = new Monkey(...
开头)并将 "addressbook" 更改为您的应用程序名称。 - 猴子还支持基于 QtQuick 的应用程序,只需将 "new QtWidgetToolkit" 更改为 "new QtQuick Toolkit" 即可。
- 通过单击
squishide
的 运行 工具栏按钮来运行测试套件。
当运行猴子测试时,你应该看到应用程序已经开始运行,并且随机的用户动作被应用到它上面:随机点击按钮、在输入字段中输入随机文本、打开随机对话框等。猴子只会与对用户可见且启用的 widgets 进行交互。猴子完成的每一个动作都会记录在测试日志中。
记录猴子的动作
写入测试日志的消息是普通的脚本语句。您可以将它们写入到日志文件中,以后可以将该文件用作测试脚本,以重现猴子执行的动作,如果在测试过程中发现缺陷。为此,修改 tst_runmonkey/test.js
文件。
例如,替换以下语句
monkey.logStatement = function(s) { test.log(s); }
为
monkey.logStatement = function(s) { test.log(s); File.open("logfile.txt", "a").write(s + "\n"); }
这段新代码将确保所有猴子的动作不仅被记录到 squishide
的测试日志中,而且每个动作都被追加到名为 logfile.txt
的文件中。此文件存储在测试用例所在的目录中(例如,examples/suite_monkeytest_js/tst_runmonkey
)。
要将 logfile.txt
转换为 Squish 可以运行的测试,用 main()
函数定义包裹日志文件的内容
function main() { <logfile.txt content> }
删除任何不相关的行以加快测试运行速度。
有关更多信息,请参阅 处理猴子日志。
停止猴子
一旦猴子开始运行,它将会永远运行,或者直到你点击 Squish 控制条的 停止 工具栏按钮,或者直到猴子崩溃应用程序,或者它因为 卡住 在某处而停止响应用户命令。如果猴子崩溃应用程序或使其停止响应,猴子将停止运行,并将相应的消息写入日志文件中。
你需要给猴子运行足够的时间——尤其是如果你的自动测试单元(AUT)已经成熟且健壮。如果你想手动停止猴子,可能很难点击 停止 按钮,因为猴子一直在抓取鼠标——尝试将鼠标从 AUT 的窗口拉出来并按 Esc 键来停止测试。
如果猴子破坏了应用程序,下一步是处理生成的日志文件,以找出原因。
处理猴子日志
猴子测试运行完成后,必须检查和解释日志文件以确定问题的原因。通常,执行此操作的第一步是在应用程序的测试套件中创建一个新空白的测试用例,将猴子日志(或来自 logfile.txt
文件)中的脚本语句复制到测试脚本中的 main()
函数中,并执行测试。如果一切照预期进行,这将重现问题。
如果问题没有重现,可能意味着在猴子测试运行期间存在的某些外部因素在重新从应用程序的测试套件中运行测试时没有运作。这可能是由于问题只在特定时间(例如,上午但不包括下午),或仅在特定的硬件条件下(例如,当空闲磁盘空间少于 10MB 时)发生,或者当猴子测试期间可以访问互联网但现在不可用(或反之亦然),或者其他类似的外部因素。
注意:为了减少短暂的外在因素数量,我们强烈建议您拥有某种类型的洁净室环境(您可以随时重新创建),并在此环境中进行所有猴子测试运行。虚拟化软件,如Xen、VirtualBox或VMware,在这种操作中可以提供很大的帮助。
假设您已成功重现问题,下一步是精简脚本,使其完成尽可能少的工作以重现问题。这种精简脚本与您一开始的原始猴子测试脚本相比具有许多优点
- 执行速度更快,因为它具有更少的脚本语句。这也意味着使用它作为测试用例来查看是否修复了错误要方便得多,因为它的所需时间要少于原始猴子测试脚本。
- 这让找出重复问题更容易。有时两个不同的猴子测试会产生相同的崩溃,但方式不同。在剥离所有不相关的部分后,通常可以明确哪些操作导致了问题,并且如果这些操作在两个精简猴子测试中相同,我们知道只找到了一个问题,而不是两个。
- 这会给AUT(自动测试用例)的开发者一个很好的想法,即在他们试图修复问题时应该在源代码中查找什么地方。因此,精简测试用例可以大大减少解决问题所需的时间,因为开发人员只需关注测试中留下的语句,而不是原始猴子测试中的所有语句。
以下是一种粗略但有效的方法,可以通过注释掉脚本来减少猴子测试脚本,这是一个易于执行且不花费太多时间的方法。将脚本的前半部分注释掉并重新运行。如果问题仍然存在,则可以删除脚本的前半部分;否则,取消注释前半部分并注释掉后半部分,然后运行它。如果问题仍然存在,则可以删除后半部分;否则它在边界上,因此注释掉第一和最后一部分,然后用中间部分尝试。一旦删除了一半,重复对该部分进行相同的过程:注释掉它的一半,如果那样不会产生问题,则取消注释并尝试注释掉另一半,依此类推。虽然听起来好像有很多东西要写,但这个过程只需要重复几次(通常两次到五次删除),直到你只留下导致问题的语句。
猴子是如何工作的
在内部,猴子测试的主要逻辑封装在Monkey
类中(定义在monkey.js
中)。要实例化此类对象,必须传递一个应用程序名称以及一个“工具包对象”。在创建猴子对象后,run
函数的基本工作方式如下
- 收集一个有趣的列表,即猴子可以与之交互的对象,例如按钮(包括工具栏按钮)、输入字段、列表、表格、树等。
- 从一个列表中随机选择一个对象。该对象必须已准备好(可见和启用)。
- 为所选对象选择要执行的脚本语句;对于按钮,使用
clickButton
函数调用是合适的,输入字段可以通过调用带有随机生成文本的type
函数来自动化,等等。 - 使用用户提供的
logStatement
函数记录生成的脚本语句,然后执行该语句。
将工具包特定的步骤(组装有趣的物体列表、从列表中选择物体、生成脚本语句)提取出来,放入专门的“工具包对象”。你可以在脚本文件qtsupport.js
中找到Qt应用程序的示例实现。因此,为了使猴子能够识别新的物体种类(以便将更多有趣的物体添加到列表中),请修改相应的工具包对象函数。
如何测试国际化的Qt AUTs
Qt提供创建国际化应用程序的支持。这意味着,例如,开发者可以使用Qt创建一个应用程序,在美国语言环境(例如美国)显示英文的菜单选项和对话框标签,并在德语环境(例如德国)显示德语文本,等等。
由于Squish使用AUT对象属性——包括它们的文本——来识别对象,因此在测试国际化AUT时可能会出现问题。例如,文件菜单项在英文环境中将显示文本“文件”,在法语环境中则可能是“Fichier”。如果在英文环境中录制了AUT的测试,则它不会在西班牙语环境中播放,因为Squish将寻找英文文本的对象,而AUT的所有文本都是西班牙语。
Squish提供了三种处理国际化的方法。
自动反向翻译
这是处理国际化AUT的简单方法,尽管它也受一个重要限制的困扰。
国际化AUT的测试应该(例如,录制)使用与AUT开发相同的环境。例如,如果AUT在美国开发,其中所有文本都是英文,则应在英语语言环境中创建测试。原始英文文本存储在AUT中,在AUT在不同的环境中运行时可能可访问,例如,在瑞典,显示菜单选项和对话框标签的瑞典文本。
我们可以告诉Squish使用AUT的原始(例如,英语)文本,即使在不同的环境中(例如,瑞典),也可以通过将环境变量SQUISH_TRANSLATION_AWARE_LOOKUP
设置为1来实现。(参见,环境变量。)
在某些情况下,相同的文本可能需要根据上下文的不同而有不同的翻译。为了支持这一点,Qt的国际化功能,QObject.tr,允许使用第二个字符串来区分。不幸的是,与原始文本不同,区分文本不存储在AUT中,因此Squish无法知道要使用哪个区分后的文本。避免此问题的唯一方法是不要使用区分文本或使用以下介绍的其他测试国际化AUT的方法之一。
使用对象名称而不是文本
解决这个问题的最简单方法是,AUT的开发者使用Qt的QObject.setObjectName方法为创建的AUT对象赋予唯一的名称。赋予该方法的文本所使用的语言无关紧要,因为它不会被翻译,因此在AUT运行在任何环境中时都保持不变。以下是如何实现这一点的示例:
fileMenu = new QMenu(tr("File")); // Text will be translated fileMenu->setObjectName("file_menu_mainwindow"); // Text won't be translated
遗憾的是,这并不是故事的终结,因为即使为 Qt 对象赋予了明确的名称,Squish 仍然继续使用它们的文本属性。解决这一问题的方法之一是简单地从 Squish 对象名称中移除所有属性,除了 type
和 name
属性(name
属性是 Squish 对 Qt 对象名称属性的称呼)。更方便的解决方案是控制 Squish 生成对象名称的方式,使其自动仅使用 type
和 name
属性为具有非空 Qt 对象名称的 AUT 对象,对于具有空 Qt 对象名称的对象则回退到使用 Squish 的标准方法。请参阅对象名称生成。
程序性翻译对象名称
处理国际化 AUT 的另一方法是在需要时自动创建特定语言的环境对象映射,并在适当的位置加载与语言相关的对象映射以替换默认对象映射。
实现此功能的一种方法是编写一个测试脚本函数(可能存储为全局脚本——参见全局脚本视图),该函数读取原始对象映射(可能使用英文文本),然后使用当前语言(例如,芬兰语)写入新的对象映射,并随后加载新创建的对象映射。
遗憾的是,仅翻译对象映射是不够的,因为“项”的文本可能不在对象映射中。对于这些情况,我们需要使用我们自己的自定义翻译函数并应用于相关文本。例如,给定记录的行
activateItem(waitForObjectItem(":File_QMenu", "Quit"));
我们需要将其更改为
activateItem(waitForObjectItem(":File_QMenu", i18n("Quit")));
假设我们的自定义翻译函数名为 i18n
。
©2024 Qt 公司有限公司。包含在此处的文档贡献是各自所有者的版权。
所提供的文档是根据自由软件基金会上发表的《GNU 自由文档许可证》第 1.3 版的条款许可的。
Qt 和相应的商标是芬兰 Qt 公司和/或其他国家的商标。所有其他商标均为各自所有者的财产。