Squish for Qt 教程

学习如何测试基于 Qt 框架的应用程序。

教程:开始测试 Qt 应用程序

Squish 搭载了一个 IDE 和命令行工具。使用 squishide 是 easiest 和 best 的开始方式,但一旦建立了大量的测试,您就会希望自动化它们。例如,执行回溯测试套件的夜间运行。因此,了解如何使用可以从批处理文件或 shell 脚本中运行的命令行工具是非常有价值的。

注意:如果您想要一些视频指导,可以在 Qt Academy(见图表)上的 45 分钟的 在线课程关于 Squish 基本用法

我们使用一个简单的地址簿应用程序作为我们的 AUT。应用程序与 Squish 一起打包在 SQUISHDIR/examples/qt/addressbook 中。这是一个非常基础的程序,允许用户加载现有的地址簿或创建一个新的地址簿,添加、编辑和删除条目,并保存(或另存为)新创建或修改的地址簿。尽管程序很简单,但它拥有大多数标准程序都有的所有关键功能:带下拉菜单的菜单栏、工具栏和一个中心区域——在这个例子中显示一个表格。它支持就地编辑,并具有用于添加条目的弹出式模态对话框。您学到的所有关于测试该应用程序的想法和实践都可以轻松应用于您自己的应用程序。有关包括模型、视图模型和视图在内的 Qt 特定功能的更多测试示例,以及所有标准编辑小部件,请参见如何创建测试脚本如何测试 Qt 应用程序

注意:在整个手册中,我们经常引用 SQUISHDIR 目录。这意味着 Squish 安装的目录,可能是 C:\Squish/usr/local/squish/opt/local/squish 或其他,具体取决于您安装的位置。确切的目录位置不重要,只要在看到手册中的路径和文件名时,将 SQUISHDIR 目录 mentally translate 为实际的目录即可。

截图显示了应用程序正在执行用户添加新姓名和地址的情况。

"The Qt addressbook example"

使用示例

第一次尝试运行示例 AUT 的测试时,您可能会收到一个以“Squish 找不到要启动的 AUT...”开头的致命错误。要从中恢复,请单击 测试套件设置 工具栏按钮,然后在选择 测试中的应用程序(AUT) 部分,如果可用,从组合框中选择 AUT,或者单击 浏览 按钮,并在查找器对话框中导航到 AUT 的可执行文件。某些版本的 Squish 如果没有指定 AUT 会自动打开此对话框。您只需要为每个示例 AUT 做一次,在测试自己的 AUT 时不做。

Squish 概念

在以下各节中,我们将创建一个测试套件,然后创建一些测试,但首先我们将简要回顾一些关键 Squish 概念。

进行测试,您需要

  1. 要测试的应用程序,称为 测试中的应用程序(AUT)
  2. 一个测试脚本,该脚本可对 AUT 进行测试。

Squish 的关键方法之一是,被测自动程序(AUT)和对其进行的测试脚本始终在两个独立的进程中执行。这确保了即使 AUT 发生崩溃,Squish 也不会崩溃。在这种情况下,测试脚本会优雅地失败并记录错误信息。除了将 Squish 和测试脚本隔离于 AUT 崩溃之外,在独立的进程中运行 AUT 和测试脚本还带来了其他好处。例如,这使得将测试脚本存储在中央位置以及在不同机器和平台上执行远程测试变得更加容易。进行远程测试的能力对于在多个平台上运行 AUT 的测试以及在对嵌入式设备进行测试特别有用。

Squish 运行一个名为 squishserver 的小型服务器,该服务器负责处理 AUT 和测试脚本之间的通信。测试脚本由 squishrunner 工具执行,该工具随后连接到 squishserver。squishserver 在设备上启动已配置的 AUT,它启动 Squish 钩子。钩子是一个小型库,它使得 AUT 的实时运行对象可访问,并允许与 squishserver 通信。在安装了钩子之后,squishserver 可以查询 AUT 对象的关于其状态的信息,并代表 squishrunner 执行命令。squishrunner 指导 AUT 执行测试脚本指定的任何操作。

所有的通信都是通过网络套接字完成的,这意味着所有操作都可以在单一机器上完成,或者测试脚本可以在一台机器上执行,而 AUT 可以在网络上的另一台机器上进行测试。

以下图示说明了 Squish 各个工具之间如何协同工作。

"Squish tools"

从测试工程师的角度来看,这种分离是不可感知的,因为所有通信都是后台透明处理的。

可以使用 squishide 编写和执行测试,在这种情况下,squishserver 会自动启动和停止,测试结果会显示在 squishide测试结果视图 中。以下图示说明了当使用 squishide 时幕后发生的情况。

"Squish IDE"

Squish 工具也可以在没有 squishide 的情况下从命令行使用。如果更喜欢使用自己的工具,例如最喜欢的编辑器,或者想要执行自动批处理测试,这非常有用。例如,在进行回归测试时。在这些情况下,必须手动启动 squishserver,所有测试完成后停止,或者为每个测试启动和停止。

为了使测试脚本能够查询和控制 AUT,Squish 必须能够访问 AUT 的内部,这可以通过使用 绑定 实现。绑定实际上是库,提供对从 GUI 工具包或从 AUT 本身可访问的对象——进而到对象的属性和方法——的访问。

为 Qt 的大部分内置类提供了 Squish 的 绑定(当向库中添加新类时,可能需要提供额外的支持)。这意味着 Qt 提供的所有标准对象(包括 GUI 小部件)都可以通过 Squish 测试脚本进行查询和控制。

实际应用中很少需要为 AUT 制作特定绑定,但如果有必要,Squish 提供一个工具使此过程尽可能简单。该工具 squishidl 用于对 AUT(以及任何附加组件)进行配置,以生成特定于 AUT 的绑定。生成的绑定库无缝集成到标准 GUI 工具包绑定中,并以相同的方式自动按需由 Squish 测试工具加载。

当 Squish 创建对 AUT 类的绑定时,对于 Qt 应用程序这意味着自动找到并使测试脚本可访问 AUT 的自定义 QObjectQ_GADGET 的属性和槽。

制作可测试的应用程序

在大多数情况下,为了使应用程序可测试,不需要做任何特别的事情,因为工具包的API(例如Qt)提供了足够的函数来实现和记录测试脚本。在启动AUT后,squishide还会自动建立到squishserver的连接。

创建测试套件

测试套件是一组一个或多个测试用例(测试)。使用测试套件很方便,因为它使得在相关测试组之间共享脚本和测试数据变得容易。

在这里,以及在整个教程中,我们将首先描述如何使用squishide来完成事情,随后跟上命令行用户的信息。

squishide创建测试套件

启动squishide,通过单击或双击squishide图标,从任务栏菜单启动squishide,或者 execute squishide 在命令行,您喜欢哪种方式,哪种方式适合您所使用的平台。Squish启动后,您可能会看到一个欢迎页面。点击右上角的工作台按钮将其关闭。然后,squishide看起来将类似于屏幕截图,但可能由于您使用的窗口系统、颜色、字体和主题而略有不同。

"The Squish IDE with no Test Suites"

Squish启动后,点击文件 > 新建测试套件将弹出下方的新建Squish测试套件向导

"Name & Directory page"

为您的测试套件输入一个名称,并选择您想要存储测试套件的文件夹。在屏幕截图中,我们命名测试套件为suite_py,并将其放置在addressbook文件夹中。(对于您自己的测试,您可能会使用更有意义的名字,例如"suite_addressbook";我们选择"suite_py",因为在本教程中我们将创建多个套件,每个套件对应Squish支持的每种脚本语言。)当然,您可以选择您喜欢的任何名称和文件夹。完成详细信息后,点击下一步继续到工具包(或脚本语言)页面。

"Toolkit page"

如果您得到此向导页面,点击您的AUT使用的工具包。对于本例,我们必须点击Qt,因为我们正在测试一个Qt应用程序。然后点击下一步转到脚本语言页面。

"Scripting Language page"

选择您想要的任何脚本语言——唯一的要求是每个测试套件只能使用一种脚本语言。(因此,如果您想使用多种脚本语言,只需创建多个测试套件,每个套件对应您想使用的每种脚本语言。)Squish针对所有语言提供的功能相同。选择脚本语言后,再次点击下一步,然后进入向导的最后一页。

"AUT page"

如果您正在为Squish已知AUT创建新的测试套件,只需点击下拉框以展开AUT列表,并选择您想要的即可。如果下拉框为空或您的AUT未列出,请点击下拉框右侧的浏览按钮——这将弹出文件打开对话框,您可以从其中导航到SQUISHDIR/examples/qt/addressbook/以选择您的AUT。

注意:不要选择quickaddressbook,它是用完全不同的API编写的。

对于Qt程序,AUT是应用程序的可执行文件(例如,Windows上的addressbook.exe)。一旦您选择了AUT,点击完成,Squish将创建一个与测试套件同名子文件夹,并在其中创建一个名为suite.conf的文件,该文件包含测试套件的配置详细信息。Squish还将AUT注册到squishserver。然后,向导将关闭,squishide将类似于下面的屏幕截图。

"The suite_py test suite"

我们现在可以开始创建测试了。

记录测试和验证点

Squish使用为测试套件指定的脚本语言记录测试。一旦测试被记录,我们就可以运行测试,Squish将忠实于我们在记录测试时执行的所有操作,但没有人类容易出现的暂停,而电脑不需要这些暂停。我们也可以编辑已记录的测试,或将已记录测试的某些部分复制到手动创建的测试中,如教程后面将要看到的。

记录将添加到现有测试用例中。您可以通过以下方式创建一个新脚本测试用例

  • 选择文件 > 新测试用例打开新Squish测试用例向导,输入测试用例名称,并选择完成
  • 单击位于测试用例标签右侧的新脚本测试用例 )工具栏按钮。这将创建一个新的测试用例,具有默认名称,您可以轻松地更改它。

为新测试用例命名 "tst_general"。

Squish会在测试套件文件夹内自动创建一个以此名称命名的子文件夹,以及一个测试文件,例如test.py。如果您选择JavaScript作为脚本语言,该文件将称为test.js,对于Perl、Ruby或Tcl也是如此。

"The tst_general test case"

如果您得到一个示例.feature文件而不是“Hello World”脚本,请单击运行测试套件 )左侧的箭头,选择新脚本测试用例 )。

要使测试脚本文件(例如,test.jstest.py)在编辑视图中显示,请单击或双击测试用例,具体取决于首选项 > 常规 > 打开模式设置。这选择脚本作为活动项目,并使其相应的记录 )和运行测试 )按钮可见。

复选框用于控制在点击运行测试套件 )工具栏按钮时运行哪些测试用例。我们还可以通过单击测试用例的运行测试 )按钮来运行单个测试用例。如果测试用例当前未处于活动状态,按钮可能不可见,直到鼠标悬停在其上方。

最初,脚本的main()Hello World记录到测试结果中。为了创建测试(如本教程后面将看到的),我们必须创建一个main函数,并在顶部输入相同的导入命令。名称main对Squish来说具有特殊含义。测试可以包含尽可能多的函数和其他代码,这由脚本语言支持,但在执行测试时(即运行),Squish总是执行main函数。您可以根据在如何创建和使用共享数据和共享脚本中描述的方法在测试脚本之间共享常用的代码。

还有另外两个函数名称对Squish来说也很特殊:cleanupinit。有关更多信息,请参阅测试人员创建的特殊函数

一旦创建了新的测试用例,我们就自由地写测试代码或记录测试。单击测试用例的记录 )按钮将替换测试的代码为新记录。或者,您可以记录片段并将其插入到现有测试用例中,如如何编辑和调试测试脚本中所述。

从命令行创建测试

从命令行创建新的测试用例是一个简单的两步过程:首先,创建一个测试用例目录;其次,创建一个带有相同元素(导入,main()函数)的测试用例脚本,就像在squishide创建同类语言的hello-world脚本时所做的。

记录我们的第一个测试

在我们深入记录之前,让我们简要回顾一下我们非常简单的测试场景

  1. 打开MyAddresses.adr地址文件。
  2. 导航到第二个地址,然后添加一个新的名字和地址。
  3. 导航到第四个地址(曾是第三个地址),然后更改姓氏字段。
  4. 导航到第一个地址,然后删除它。
  5. 验证第一个地址现在是中国添加的新地址。

我们现在可以记录我们的第一个测试。单击位于测试套件视图测试用例列表中tst_general测试用例右侧的录制测试用例工具栏按钮()。这将使Squish运行AUT,以便我们可以与之交互。一旦AUT运行,请执行以下操作——不要担心它需要多长时间,因为Squish不会记录空闲时间

  1. 点击文件 > 打开,一旦文件对话框出现,单击MyAddresses.adr文件名,然后单击打开按钮。
  2. 单击第二行,然后单击编辑 > 添加。在添加对话框的第一行中键入“Jane”,现在点击(或使用光标左右移动到)第二行编辑并键入“Doe”。继续类似地,将电子邮件地址更改为“[email protected]”,并将电话号码更改为“555 123 4567”。不必担心打字错误——按正常方式使用退格键并修复错误。最后,单击确定按钮。现在应该有一个包含您输入的详细信息的新第二个地址。
  3. 单击第四行的第二个(姓氏)列,删除其文本并用“Doe”替换。(您可以简单地通过输入替换。)然后按Enter键确认您的编辑。
  4. 单击第一行,然后单击编辑 > 删除,然后在消息框中单击按钮。最后,您的“Jane Doe”条目应该位于第一行。
  5. 在Squish 控制栏窗口(从左数第二个按钮)中单击验证工具栏按钮,并选择属性

    {}

    这将使squishide出现。在应用程序对象视图中展开MainWindow对象,然后是QTableWidget对象。单击“item_0/0”QModelIndex对象以使其属性出现在属性视图中,然后检查text属性的复选框。现在单击“item_0/1”QModelIndex并检查其text属性。最后,单击(验证点创建视图底部的)保存并插入验证按钮,将第一行的姓名和姓氏验证插入到记录的测试脚本中。(请参阅下面的截图。)一旦插入验证点,squishide的窗口将再次隐藏,而控制栏窗口和AUT将再次出现在视图中。

  6. 我们已经完成了测试,因此,在AUT中单击文件 > 退出,然后在消息框中单击,因为我们不希望保存任何更改。

"Two verification points about to be inserted"

一旦我们退出AUT,记录的测试将如截图所示显示在 squishide 中。(注意,所记录的确切代码会根据您的交互方式而变化。例如,您可以通过点击或使用按键序列来调用菜单选项——使用哪种方式无关紧要,但由于它们不同,Squish会以不同的方式记录它们。)

"The recorded tst_general test"

如果未出现记录的测试,请点击(或根据您的平台和设置双击)tst_general 测试用例;这将使Squish在编辑器窗口中显示测试的 test.py 文件,如截图所示。

我们现在已记录了测试并能播放回放,即运行它。这本身就是有用的,因为如果回放失败,可能意味着应用程序已被破坏。此外,我们放置的两个验证将在回放时进行检查,如截图所示。

在测试记录过程中插入验证点是十分方便的。在此,我们一次性插入了两个,但您可以在记录过程中任意次数地插入任意数量的验证点。然而,有时我们可能会忘记插入验证点,或者在之后可能想要插入新的验证点。我们将像在下一节中看到的那样,可以轻松地将附加验证插入到记录的测试脚本中,下一节标题为《插入附加验证点》。

在进一步操作之前,我们将了解如何运行测试,并查看Squish录制测试生成的部分代码,并讨论其中的某些功能。

从IDE运行测试

squishide 中运行测试用例,当测试用例在 测试套件视图 中被悬停或选中时,点击出现的 运行测试)。

要一个接一个地运行两个或更多测试用例,或者只运行选中的测试用例,点击 运行测试套件)。

从命令行运行测试

在运行测试时,squishserver必须始终处于运行状态,或者必须向squishrunner提供 --local 选项。有关更多信息,请参阅 squishserver

要从命令行回放记录的测试,我们可以执行squishrunner程序并指定测试套件目录和您要回放的测试用例。例如,假设我们在包含测试套件目录的目录中

squishrunner --testsuite suite_py --testcase tst_general --local

检查生成的代码

如果您查看截图中的代码(或下面的代码片段),您会看到它由许多 Object waitForObject(objectOrName) 调用组成,作为各种其他调用的参数,例如 activateItem(itemObject)clickButton(objectOrName)clickItem(objectOrName, itemOrIndex, x, y, modifierState, button)type(objectOrName, text)。Object waitForObject(objectOrName) 函数等待一个GUI对象准备交互(即变为可见并启用),然后会跟随一些与对象交互的函数。典型的交互包括激活(弹出)菜单、单击菜单选项或按钮,或输入一些文本。

要全面了解Squish的脚本命令,请参阅《如何创建测试脚本》(How to Create Test Scripts),《如何测试应用程序 - 特殊方面》(How to Test Applications - Specifics),《API参考手册》(API Reference),以及《工具参考手册》(Tools Reference)。

对象通过Squish生成的名称来识别。有关详细信息,请参阅《如何识别和访问对象》(How to Identify and Access Objects)。

生成的代码大约有35行。以下是一个摘录,仅显示了Squish如何记录编辑菜单中添加选项的点击操作,将Jane Doe的详细信息输入到添加对话框中,并在最后点击OK按钮关闭对话框更新表格。

注意:虽然截图只显示了Python测试套件的运行情况,但对于这里引用和相关教程中的代码片段,我们展示了Squish支持的所有脚本语言的代码。在实际操作中,您当然通常只会使用其中一种,所以请自由地查看您感兴趣的语言的代码片段,并跳过其他语言。

    clickButton(waitForObject(names.address_Book_MyAddresses_adr_Add_QToolButton))
    snooze(.5)  # qt4/linux workaround
    type(waitForObject(names.forename_LineEdit), "Jane")
    type(waitForObject(names.forename_LineEdit), "<Tab>")
    type(waitForObject(names.surname_LineEdit), "Doe")
    type(waitForObject(names.surname_LineEdit), "<Tab>")
    type(waitForObject(names.email_LineEdit), "[email protected]")
    type(waitForObject(names.email_LineEdit), "<Tab>")
    type(waitForObject(names.phone_LineEdit), "123 555 1212")
    clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
    clickButton(waitForObject(names.addressBookMyAddressesAdrAddQToolButton));
    type(waitForObject(names.forenameLineEdit), "Jane");
    type(waitForObject(names.forenameLineEdit), "<Tab>");
    type(waitForObject(names.surnameLineEdit), "Doe");
    type(waitForObject(names.surnameLineEdit), "<Tab>");
    type(waitForObject(names.emailLineEdit), "[email protected]");
    type(waitForObject(names.emailLineEdit), "<Tab>");
    type(waitForObject(names.phoneLineEdit), "123 555 1212");
    clickButton(waitForObject(names.addressBookAddOKQPushButton));
    clickButton(waitForObject($Names::address_book_myaddresses_adr_add_qtoolbutton));
    type(waitForObject($Names::forename_lineedit), "Jane");
    type(waitForObject($Names::forename_lineedit), "<Tab>");
    type(waitForObject($Names::surname_lineedit), "Doe");
    type(waitForObject($Names::surname_lineedit), "<Tab>");
    type(waitForObject($Names::email_lineedit), "Jane.Doe\@nowhere.com");
    type(waitForObject($Names::email_lineedit), "<Tab>");
    type(waitForObject($Names::phone_lineedit), "123 555 1212");
    clickButton(waitForObject($Names::address_book_add_ok_qpushbutton));
    clickButton(waitForObject(Names::Address_Book_MyAddresses_adr_Add_QToolButton))
    type(waitForObject(Names::Forename_LineEdit), "Jane")
    type(waitForObject(Names::Forename_LineEdit), "<Tab>")
    type(waitForObject(Names::Surname_LineEdit), "Doe")
    type(waitForObject(Names::Surname_LineEdit), "<Tab>")
    type(waitForObject(Names::Email_LineEdit), "[email protected]")
    type(waitForObject(Names::Email_LineEdit), "<Tab>")
    type(waitForObject(Names::Phone_LineEdit), "123 555 1212")
    clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
    invoke clickButton [waitForObject $names::Address_Book_MyAddresses_adr_Add_QToolButton]
    invoke type [waitForObject $names::Forename_LineEdit] "Jane"
    invoke type [waitForObject $names::Forename_LineEdit] "<Tab>"
    invoke type [waitForObject $names::Surname_LineEdit] "Doe"
    invoke type [waitForObject $names::Surname_LineEdit] "<Tab>"
    invoke type [waitForObject $names::Email_LineEdit] "[email protected]"
    invoke type [waitForObject $names::Email_LineEdit] "<Tab>"
    invoke type [waitForObject $names::Phone_LineEdit] "123 555 1212"
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]

如您所见,测试人员使用键盘在文本框之间切换,并使用鼠标而不是按键点击OK按钮。如果测试人员以任何其他方式点击按钮(例如,按Alt+O或切换到OK按钮并按空格键),结果将是相同的,但当然Squish会记录实际执行的操作。

Squish记录在引用对象时使用以names.前缀开始的变量,这将它们标识为符号名称。每个变量都包含对应的真实名称作为值,这可以是基于字符串的,也可以实现为属性到值的键值映射。Squish支持几种命名方案,所有这些方案都可以在脚本中使用并混合使用。使用符号名称的优点是,如果应用程序发生变化需要不同的名称,我们可以简单地更新Squish的对象映射(将符号名称与真实名称相关联),从而避免需要更改我们的测试脚本。有关对象映射的更多信息,请参阅Object MapObject Map view

当符号名称位于光标下时,编辑器的上下文菜单允许您打开符号名称,显示它在对象映射中的条目,或者转换为真实名称,在脚本语言中将光标处的行内映射放置到您的脚本中,允许您直接在脚本中编辑属性。

既然我们已经了解了如何记录和播放测试,并看到了Squish生成的代码,让我们进一步确保在测试执行的特定点满足某些条件。

插入额外的验证点

在上一节中,我们看到了在记录测试脚本期间插入验证点有多容易。验证点也可以插入到现有的测试脚本中,通过设置断点并使用squishide,或者只需编辑测试脚本并在其中放入对Squish测试函数如test.comparetest.verify的调用即可。

Squish支持许多种验证点类型:那些验证对象属性是否具有特定值——称为“对象属性验证”;验证整个表格内容是否与预期相符——称为“表格验证”;验证两张图片是否匹配——称为“截图验证”;以及一种混合验证类型,它包括多个对象的属性和截图,称为“视觉验证”。此外,我们还可以验证搜索图片是否出现在屏幕上的某些位置,或某些文本是否通过OCR识别。最常用的是对象属性验证,教程将介绍这一部分。更多阅读请参考:如何创建和使用验证点

常规(非脚本化)属性验证点以XML文件的形式存储在测试案例或测试套件资源中,包含需要传递给test.compare的值。这些验证点可以在多个测试案例中重复使用,可以在一行脚本代码中验证多个值。

脚本化属性验证点是对test.compare函数的直接调用,包含两个参数——特定对象的特定属性的值和预期值。我们可以在录制或手写的脚本中手动插入对test.compare函数的调用,或者使用脚本化验证点让Squish为我们插入。在前一节中,我们展示了如何使用squishide在录制过程中插入验证。在这里,我们将首先展示如何使用squishide将验证插入到现有的测试脚本中,然后我们将展示如何手动插入验证。

在请求Squish插入验证点之前,最好是有一个要验证的内容及其时间的列表。我们可以向tst_general测试案例中添加许多潜在的验证,但鉴于我们这里只是简单展示如何操作,我们只会做两项——我们将验证“Jane Doe”条目的电子邮件地址和电话号码是否与输入的一致,并将验证直接放在我们录制时插入的验证之后。

要使用squishide插入验证点,我们首先在脚本中设置一个断点(无论是录制的还是手动写的——对Squish来说无所谓),在我们要验证的位置。

"The tst_general test case with a breakpoint"

如图所示,我们已在第35行设置了断点。这可以通过双击或在编辑器的空白栏(行号旁边)右键单击并选择添加断点上下文菜单项来完成。我们选择这一行,因为它紧随删除第一个地址的脚本行,因此在这一刻(在与调用文件菜单关闭应用程序之前),第一个地址应该是“Jane Doe”的。截图显示了使用squishide在录制过程中输入的验证。我们额外的验证将跟随它们。(注意,如果你的测试是以不同的方式录制的,例如,使用快捷键而不是菜单项,你的行号可能不同。)

设置断点后,我们像往常一样运行测试,通过单击运行测试)按钮,或者通过单击运行 > 运行测试案例菜单选项。与正常测试运行不同,测试将在达到断点时停止(即在第33行或你设置的行),Squish的主窗口将重新出现(这可能会遮挡AUT)。此时,squishide将自动切换到测试调试视角

视角和视图

squishide 的工作方式与 Eclipse IDE 相同。如果您不熟悉 Eclipse,了解以下关键概念至关重要:视图视角。在 Eclipse 及其 squishide 中,一个 视图 实质上是一个子窗口,如停靠窗口或现有窗口中的选项卡。一个 视角 是一组排列在一起的视图。两者都可通过 窗口 菜单访问。

squishide 随附以下视角

您可以根据需要更改这些视角以显示更多视图或隐藏不需要的视图,或创建自己的视角,该视角恰好包含您想要的视图。因此,如果您的窗口发生巨大变化,这只意味着视角已更改。使用 窗口 菜单更改到您想要的视角。然而,Squish 会自动更改视角以反映当前情况,所以您通常不需要手动更改视角。

插入验证点

如以下屏幕截图所示,当 Squish 在断点处停止时,squishide 将自动切换到 测试调试视角。该视角显示 变量视图编辑器视图调试视图应用程序对象视图属性视图方法视图 以及 测试结果视图

要插入验证点,我们可以在应用程序对象视图中展开条目,直到找到要验证的对象。在这个例子中,我们想验证 QTableWidget 的第一行的文本,所以我们展开“地址簿 - MyAddresses_adr”条目,以及其子项,直到找到 QTableWidget,然后在其内部找到我们感兴趣的条目。一旦我们单击条目对象,其属性就像截图所示一样在 属性视图 中显示出来。

"Picking an object to verify in the Application Objects view"

您可以通过从 窗口 菜单中选择它(或单击其工具栏按钮)在任何时候返回到正常的 测试管理视角,尽管当您停止脚本或运行到完成时,squishide 将自动返回到它。

在这里,我们可以看到行 0 和列 0 中条目的 text 属性的值是 "Jane";我们在录制过程中已经为它添加了一个验证。向下滚动,以便您可以看到行 0 列 2 的条目:这是电子邮件地址。要确保测试运行时每次都验证它,请点击 应用程序对象视图 中的 "item_0/2" 条目以使其属性出现,然后点击 text 属性以检查其复选框。当我们检查它时,验证点创建器视图 就会出现,如图所示。

"Choosing a property value to verify"

此时,验证点尚未添加到测试脚本中。我们可以通过单击 保存并插入验证 按钮轻松添加它。但在这样做之前,我们会添加另一需要验证的条目。

向下滚动并点击“应用程序对象视图”中的“item_0/3”项。然后点击其“文本”属性。现在,正如截图所示,这两个验证都将出现在“验证点创建视图”中。

"Choosing several property values to verify"

我们现在说明了我们期望这些属性有显示的值,即电子邮件地址为“[email protected]”和电话号码为“555 123 4567”。我们必须点击“保存并插入验证”按钮以实际插入验证点,所以现在就这样做。

我们现在不需要继续运行测试,所以我们可以点击“停止”工具栏按钮停止运行测试,或者我们可以继续(点击“恢复”按钮)。

一旦我们插入完验证并停止或完成测试,我们现在应该禁用断点。只需右键单击断点并点击上下文菜单中的“禁用断点”菜单选项。我们现在可以准备运行没有断点但有验证点的测试。点击“运行测试”)按钮。这次我们将得到一些额外的测试结果——正如截图所示——其中之一我们已经展开以显示其详细信息。(我们还选择了Squish插入以执行验证的代码行——请注意代码的结构与记录期间插入的代码相同。)

"The newly inserted verification points"

这些特定的验证点生成四个测试,比较新插入条目的名、姓、电子邮件和电话号码。

插入验证点的另一种方法是在代码形式中插入。从理论上讲,我们只需在我们喜欢的现有脚本中任意位置添加对我们自己的Squish测试函数的调用,如test.comparetest.verify。在实践中,最好确保Squish在我们验证之前知道我们想要验证的对象,这样在运行测试时它可以找到它们。这涉及到一个非常类似的步骤,就像我们使用squishide插入它们一样。首先我们设置一个断点,在那里我们打算添加我们的验证。然后运行测试脚本直到它停止。接下来我们在“应用程序对象视图”中导航,直到找到我们想要验证的对象。在此情况下,右键单击我们感兴趣的对象并点击上下文菜单中的“添加到对象映射”菜单选项是明智的。这将确保Squish可以访问该对象。然后再次右键单击,并点击上下文菜单中的“复制符号名”菜单选项——这给我们Squish将用于识别对象的名称。现在我们可以编辑测试脚本以添加我们的验证并完成或停止执行。(别忘了一旦不再需要就禁用断点。)

尽管我们可以编写代码以与自动生成的代码完全相同,但通常以稍不同的样式进行操作更清晰、更容易,我们稍后会解释这一点。

对于我们的手动验证,我们想在读取MyAddresses.adr文件后、在添加新地址后,以及删除第一个地址后,检查QTableWidget中存在的地址数量。截图显示了我们的代码行之一,用于获取这三个验证之一,以及测试脚本的运行结果。

"Manually entered verification points"

在手动编写脚本时,我们使用Squish的test模块函数来验证在测试脚本执行过程中的某些条件。如图表(以及下面的代码片段)所示,我们首先检索我们感兴趣的物体的引用。使用Object waitForObject(objectOrName)函数是手动编写测试脚本的常规做法。此函数等待对象可用(即,可见和启用),然后返回对其的引用。(否则它会超时并抛出可捕获的异常。)然后我们使用此引用来访问项的属性——在这种情况下是QTableWidget的rowCount属性——然后使用test.compare函数验证值是否是我们期望的。(顺便说一下,我们从上一行获取了物体的名称,因此我们不必设置断点并手动将表的名称添加到对象映射表中,以确保Squish会记住在本例中,因为Squish在测试录制期间已将其添加。)

以下是我们在所有Squish支持的语言中手动输入的第一次验证的代码。当然,您只需要查看您将用于自己的测试的代码。对于其他验证,我们只是调用了test.compare函数,并重复使用在下面的代码中获得的table对象引用。

    table = waitForObject(names.address_Book_MyAddresses_adr_File_QTableWidget)
    test.compare(table.rowCount, 125)
    var table = waitForObject(names.addressBookMyAddressesAdrFileQTableWidget)
    test.compare(table.rowCount, 125);
    my $table = waitForObject($Names::address_book_myaddresses_adr_file_qtablewidget);
    test::compare($table->rowCount, 125);
    table = waitForObject(Names::Address_Book_MyAddresses_adr_File_QTableWidget)
    Test.compare(table.rowCount, 125)
    set table [waitForObject $names::Address_Book_MyAddresses_adr_File_QTableWidget]
    test compare [property get $table rowCount] 125

编码模式非常简单:我们检索对我们感兴趣的物体的引用,然后使用Squish的某种验证函数验证其属性。当然,如果我们想,我们也可以调用对象的函数与之交互。

有关手动编写的测试的更多示例,请参阅手动创建测试如何创建测试脚本如何测试应用程序 - 特定细节

有关验证点的完整覆盖范围,请参阅如何创建和使用验证点

测试结果

每次测试运行结束后,测试结果(包括验证点的结果)会在squishide底部的测试结果视图中显示。

这是一份详细的测试运行报告,它也会包含失败的详细失败或其他错误等信息。如果您单击测试结果项,squishide会突出显示生成测试结果的脚本行。如果您展开测试结果项,您可以看到测试的更多详细信息。

Squish的测试结果界面非常灵活。通过实现自定义报告生成器,可以以许多不同的方式处理测试结果,例如将它们存储在数据库中,或者以HTML文件的形式输出。默认报告生成器简单地当Squish从命令行运行时打印结果到stdout,或者当使用squishide时到测试结果视图。您可以通过右键单击测试结果并选择导出结果菜单项将测试结果从squishide保存为XML。有关报告生成器的列表,请参阅squishrunner - reportgen: 生成报告。您也可以直接将测试结果记录到数据库。请参阅如何从Squish测试脚本中访问数据库

如果您使用squishrunner在命令行上运行测试,您也可以以不同的格式导出结果并将它们保存到文件中。有关更多信息,请参阅处理测试结果如何使用测试语句部分。

手动创建测试

既然我们已经看到了如何通过插入验证点来记录和修改测试,我们现在可以看看如何手动创建测试。做得最容易的方式是修改和重构已记录的测试,尽管从头开始创建手动测试也是完全可能的。

手动编写测试最具挑战性的部分可能是正确使用对象名称,但在实践中,这很少成为问题。我们可以复制Squish在记录前一次测试时已添加到对象映射的符号名称,或者直接从已记录的测试中复制对象名称。如果我们还没有录制任何测试,而是从头开始,我们可以使用间谍工具。这样做是通过点击工具栏上的启动应用程序按钮。这将启动AUT并切换到间谍视口。然后,我们可以与AUT交互,直到我们感兴趣的物体可见。接下来,在squishide内部,我们可以导航到,或者选择该物体,使其在应用程序对象视图中被选中,并使用上下文菜单来完成添加到对象映射复制(符号名 | 实际名称)到剪贴板(这样我们就可以将它粘贴到测试脚本中)。最后,我们可以点击工具栏上的退出应用程序按钮来终止AUT并将Squish切换回测试管理视口。有关使用间谍的更多详细信息,请参阅如何使用间谍

我们可以通过点击工具栏上的对象映射按钮来查看对象映射,或者从脚本编辑器的上下文菜单打开:打开符号名称,当在脚本中对一个对象名称进行右键单击时(参见对象映射视图)。Squish与之交互的每个应用程序对象都列在此处,要么是顶级对象,要么是子对象(视图是树状视图)。我们可以通过右键单击我们感兴趣的物体然后点击上下文菜单的复制符号名称(以获取符号名称变量)或复制实际名称(以获取存储在变量中的实际键值对)来检索Squish在已记录脚本中使用的符号名称。这在我们需要修改现有的测试脚本或从头开始创建测试脚本时很有用,就像我们在教程后面将要看到的那样。

"Squish's Object Map"

修改和重构已记录的测试

假设我们想要通过添加三个新的名称和地址来测试AUT的添加功能。我们当然可以记录这样的测试,但是在代码中完成所有事情也是同样容易的。测试脚本需要进行以下步骤:首先点击文件 > 新建创建一个新的通讯录,然后对于每个新的名称和地址,点击编辑 > 添加,然后填写详细信息,并点击确定。最后,不要保存就点击文件 > 退出。我们还想在开始时验证没有行数据,结束时有三行。我们还将进行重构,尽可能让我们的代码整洁且模块化。

首先,我们必须创建一个新的测试用例。点击文件 > 新建测试用例将测试用例的名称设置为tst_adding。Squish将自动创建一个test.py(或者test.js等等)文件。

命令行用户只需在测试套件的目录内创建一个名为tst_adding的目录,并在该目录中创建和编辑test.py文件(或者test.js等等)。

我们首先需要一种启动自动测试(AUT)并调用菜单选项的方法。下面是从录制tst_general脚本中提取的前几行。

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/addressbook/addressbook"')
    activateItem(waitForObjectItem(names.address_Book_QMenuBar, "File"))
    activateItem(waitForObjectItem(names.address_Book_File_QMenu, "Open..."))
import * as names from 'names.js';

function main() {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/addressbook/addressbook"');
    activateItem(waitForObjectItem(names.addressBookQMenuBar, "File"));
    activateItem(waitForObjectItem(names.addressBookFileQMenu, "Open..."));
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/addressbook/addressbook\"");
    activateItem(waitForObjectItem($Names::address_book_qmenubar, "File"));
    activateItem(waitForObjectItem($Names::address_book_file_qmenu, "Open..."));
require 'names'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/addressbook/addressbook\"")
    activateItem(waitForObjectItem(Names::Address_Book_QMenuBar, "File"))
    activateItem(waitForObjectItem(Names::Address_Book_File_QMenu, "Open..."))
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/addressbook/addressbook\""
    invoke activateItem [waitForObjectItem $names::Address_Book_QMenuBar "File"]
    invoke activateItem [waitForObjectItem $names::Address_Book_File_QMenu "Open..."]

代码中的模式很简单:启动AUT,然后等待菜单栏,然后激活菜单栏;等待菜单项,然后激活菜单项。在两种情况下我们都使用了Object waitForObjectItem(objectOrName, itemOrIndex)函数。这个函数用于多值对象(如列表、表格、树——或者在这个例子中,是菜单栏和菜单),并且允许我们通过传递包含项目的对象名称和项目文本作为参数来访问对象的项目(当然,这些项目 themselves 也是对象)。

注意:将函数放入tst_adding中似乎有些浪费,因为我们还可以在tst_general和其他测试用例中使用它们。然而,为了使教程简单,我们将代码放在tst_adding测试用例中。请参阅如何创建和使用共享数据和共享脚本了解如何共享脚本。

如果您查看录制的测试用例(tst_general)或对象映射表,您会发现Squish有时会为同一事物使用不同的名称。例如,菜单栏以三种不同的方式被识别,最初是AddressBook_QMenuBar,然后如果用户点击文件 > 新建,它被识别为AddressBook_Unnamed_QMenuBar,如果用户点击文件 > 打开并打开MyAddresses.adr文件,那么菜单栏被识别为AddressBook_MyAddressesadr_QMenuBar。原因是Squish需要在同一上下文中唯一地识别每个对象,它使用手头拥有的任何信息。因此,在识别菜单栏(以及许多其他对象)的情况下,Squish使用窗口标题文本以提供上下文。(例如,一个应用程序的文件或编辑菜单可能因是否加载了文件以及应用程序处于何种状态而具有不同的选项。)

当然,当我们编写测试脚本时,我们不想知道或关心使用某一个特定名称变体,Squish通过提供替代的命名方案来支持这一需求,我们将在不久后看到。

如果在测试执行过程中AUT看起来已冻结,请等待Squish超时AUT(大约20秒),并显示找不到对象对话框,显示像这样的错误:

"Object Not Found dialog"

这通常意味着Squish在对象映射表中没有具有给定名称的对象或属性值。从这里,我们可以在选择新对象调试引发错误或选择新对象后,重试

选择新对象将更新符号名称的对象映射条目。除了对象选择器)外,我们还可以使用Spy的应用程序对象视图来定位我们感兴趣的对象,并使用添加到对象映射表的上下文菜单动作来访问它们的真实或符号名称。

命名很重要,因为这可能是编写脚本中最容易导致错误消息的部分,通常是上述提到的对象...未找到。一旦我们确定了测试中要访问的对象,使用Squish编写测试脚本就非常简单。特别是,Squish很可能支持您最熟悉的脚本语言。

我们现在几乎准备好编写自己的测试脚本了。最简单的方法可能是先记录一个模拟测试。因此,请点击 文件 > 新建测试用例,将测试用例的名称设置为 tst_dummy。然后点击模拟测试用例的 记录(《img alt="" src="images/record.png"》)。

当自动化测试工具(AUT)启动后,点击 文件 > 新建,然后点击(空的)表格,接着点击 编辑 > 添加 并添加一项,然后按 Return 键或点击 确定。最后,点击 文件 > 退出 完成,并接受 保存更改。这样做的主要目的是确保 Squish 将必要的名称添加到对象映射中,因为这种方法可能比为每个感兴趣的对象使用 Spy 快得多。在重新播放模拟测试后,如果您愿意,可以删除它。

在对象映射中有我们需要的所有对象名称后,我们现在可以从头开始编写自己的测试脚本。我们将从 main 函数开始,然后我们将查看 main 函数使用的支持函数。

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/addressbook/addressbook"')
    invokeMenuItem("File", "New")
    table = waitForObject({"type": "QTableWidget"})
    test.verify(table.rowCount == 0)
    data = [("Andy", "Beach", "[email protected]", "555 123 6786"),
            ("Candy", "Deane", "[email protected]", "555 234 8765"),
            ("Ed", "Fernleaf", "[email protected]", "555 876 4654")]
    for oneNameAndAddress in data:
        addNameAndAddress(oneNameAndAddress)
    waitForObject(table);
    test.compare(table.rowCount, len(data), "table contains as many rows as added data")
    closeWithoutSaving()
import * as names from 'names.js';

function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"type": "QMenuBar"}, menu));
    activateItem(waitForObjectItem({"title": menu, "type": "QMenu"}, item));
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/addressbook/addressbook\"");
    invokeMenuItem("File", "New");
    my $table = waitForObject({"type" => "QTableWidget"});
    test::verify($table->rowCount == 0);
    my @data = (["Andy", "Beach", "andy.beach\@nowhere.com", "555 123 6786"],
                ["Candy", "Deane", "candy.deane\@nowhere.com", "555 234 8765"],
                ["Ed", "Fernleaf", "ed.fernleaf\@nowhere.com", "555 876 4654"]);
    foreach my $oneNameAndAddress (@data) {
        addNameAndAddress(@{$oneNameAndAddress});
    }
    test::compare($table->rowCount, scalar(@data), "table contains as many rows as added data");
    closeWithoutSaving();
}
require 'names'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/addressbook/addressbook\"")
    invokeMenuItem("File", "New")
    table = waitForObject({:type => "QTableWidget"})
    Test.verify(table.rowCount == 0)
    data = [["Andy", "Beach", "[email protected]", "555 123 6786"],
        ["Candy", "Deane", "[email protected]", "555 234 8765"],
        ["Ed", "Fernleaf", "[email protected]", "555 876 4654"]]
    data.each do |oneNameAndAddress|
        addNameAndAddress(oneNameAndAddress)
    end
    Test.compare(table.rowCount, data.length, "table contains as many rows as added data")
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/addressbook/addressbook\""
    set table [waitForObject [::Squish::ObjectName type QTableWidget]]
    test compare [property get $table rowCount] 0
    invokeMenuItem "File" "New"
    set data [list \
        [list "Andy" "Beach" "[email protected]" "555 123 6786"] \
        [list "Candy" "Deane" "[email protected]" "555 234 8765"] \
        [list "Ed" "Fernleaf" "[email protected]" "555 876 4654"] ]
    for {set i 0} {$i < [llength $data]} {incr i} {
        addNameAndAddress [lindex $data $i]
    }
    waitForObject $table
    test compare [property get $table rowCount] [llength $data] "table contains as many rows as added data"
    closeWithoutSaving
}

我们首先通过调用 ApplicationContext startApplication(autName) 函数来启动应用程序。我们作为字符串传递的名称是在 Squish 中注册的名称(通常是可执行文件名称)。然后我们获取到 QTableWidget 的引用。我们使用的对象名称是一个真实名称,包含键值对。从对象映射编辑器中,我们找到了另一个 QTableWidget 对象映射条目,并从 复制真实名称 获取它的真实名称。我们将其粘贴到我们的 tst_adding 测试用例中,并删除了过于具体的属性。

Object waitForObject(objectOrName) 函数等待一个对象准备好(可见并可启用),并返回其引用——或者在超时后抛出一个可捕获的异常。一旦我们有了 table 引用,我们就可以用它来访问 QTableWidget 的任何公共方法和属性。

invokeMenuItem 函数是我们为这个测试特别创建的。它接受一个菜单名称和一个菜单选项名称,并调用该菜单选项。它还使用真实名称来描述对象,并演示了如何从每种脚本语言的变量中参数化值。在用 invokeMenuItem 函数执行 文件 > 新建 后,我们验证表行数为 0。

接下来,我们创建一些示例数据,并调用自定义 addNameAndAddress 函数,使用 AUT 的添加对话框用数据填充表。然后我们再次比较表的行数,这次是与我们的示例数据行数进行比较。最后,我们调用自定义 closeWithoutSaving 函数来终止应用程序。

我们现在将回顾三个支持函数中的每一个,以涵盖 tst_adding 测试用例中的所有代码,从 invokeMenuItem 函数开始。

def invokeMenuItem(menu, item):
    activateItem(waitForObjectItem({"type": "QMenuBar"}, menu))
    activateItem(waitForObjectItem({'type': 'QMenu', 'title': menu}, item))
function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"type": "QMenuBar"}, menu));
    activateItem(waitForObjectItem({"title": menu, "type": "QMenu"}, item));
}
sub invokeMenuItem
{
    my ($menu, $item) = @_;
    activateItem(waitForObjectItem({"type" => "QMenuBar"}, $menu));
    activateItem(waitForObjectItem({"title" => $menu, "type" => "QMenu"}, $item));
}
def invokeMenuItem(menu, item)
    activateItem(waitForObjectItem({:type => "QMenuBar"}, menu))
    activateItem(waitForObjectItem({:title => menu, :type => "QMenu"}, item))
end
proc invokeMenuItem {menu item} {
    invoke activateItem [waitForObjectItem [::Squish::ObjectName type QMenuBar] $menu]
    invoke activateItem [waitForObjectItem [::Squish::ObjectName title $menu type QMenu] $item]
}

正如我们之前提到的,Squish 用于菜单、菜单项(以及其他对象)的对象名称可能因上下文而异,上下文部分来自窗口的标题。对于我们的可重用函数,我们希望对象名称与所需对象匹配,无论上下文或窗口标题如何。因此,我们不会重用现有的符号名称,而是会复制其实际名称,并移除我们不想检查的属性。

每个真实名称必须指定 type 属性,通常至少还需要其他一个属性。在这里,我们使用了 type 来唯一标识菜单栏,并且使用 typetitle 属性来唯一标识菜单。通过使用真实名称,我们可以创建一个通用的对象名称,无论窗口标题如何,都能与我们的目标对象匹配。

一旦我们识别出我们要与之交互的对象,我们使用 Object waitForObjectItem(objectOrName, itemOrIndex) 函数来获取对其的引用,然后在这个案例中,我们应用 activateItem(itemObject) 函数。`Object waitForObjectItem(objectOrName, itemOrIndex)` 函数会暂停 Squish,直到指定的对象和其项目可见并启用。因此,在这里,我们等待了菜单栏和其中一个菜单栏项,然后等待一个菜单栏项和其中的一项菜单栏项。只要等待完成,我们就使用 `activateItem(itemObject)` 函数激活每个对象和其项。

def addNameAndAddress(oneNameAndAddress):
    invokeMenuItem("Edit", "Add...")
    type(waitForObject(names.forename_LineEdit), oneNameAndAddress[0])
    type(waitForObject(names.surname_LineEdit), oneNameAndAddress[1])
    type(waitForObject(names.email_LineEdit), oneNameAndAddress[2])
    type(waitForObject(names.phone_LineEdit), oneNameAndAddress[3])
    clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
function addNameAndAddress(oneNameAndAddress)
{
    invokeMenuItem("Edit", "Add...");
    type(waitForObject(names.forenameLineEdit), oneNameAndAddress[0]);
    type(waitForObject(names.surnameLineEdit), oneNameAndAddress[1]);
    type(waitForObject(names.emailLineEdit), oneNameAndAddress[2]);
    type(waitForObject(names.phoneLineEdit), oneNameAndAddress[3]);

    clickButton(waitForObject(names.addressBookAddOKQPushButton));
}
sub addNameAndAddress
{
    my(@oneNameAndAddress) = @_;
    invokeMenuItem("Edit", "Add...");
    type(waitForObject($Names::forename_lineedit), $_[0]);
    type(waitForObject($Names::surname_lineedit), $_[1]);
    type(waitForObject($Names::email_lineedit), $_[2]);
    type(waitForObject($Names::phone_lineedit), $_[3]);
    clickButton(waitForObject($Names::address_book_add_ok_qpushbutton));
}
def addNameAndAddress(oneNameAndAddress)
    invokeMenuItem("Edit", "Add...")
    type(waitForObject(Names::Forename_LineEdit), oneNameAndAddress[0])
    type(waitForObject(Names::Surname_LineEdit), oneNameAndAddress[1])
    type(waitForObject(Names::Email_LineEdit), oneNameAndAddress[2])
    type(waitForObject(Names::Phone_LineEdit), oneNameAndAddress[3])
    clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
end
proc addNameAndAddress {oneNameAndAddress} {
    invokeMenuItem "Edit" "Add..."
    invoke type [waitForObject $names::Forename_LineEdit] [lindex $oneNameAndAddress 0]
    invoke type [waitForObject $names::Surname_LineEdit] [lindex $oneNameAndAddress 1]
    invoke type [waitForObject $names::Email_LineEdit] [lindex $oneNameAndAddress 2]
    invoke type [waitForObject $names::Phone_LineEdit] [lindex $oneNameAndAddress 3]
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]
}

对于每一组名称和地址数据,我们调用 `编辑 > 添加` 菜单选项来弹出 `添加` 对话框。然后对于接收到的每个值,我们通过等待相应的 `QLineEdit` 准备就绪并使用 `type(objectOrName, text)` 函数输入文本来填充合适的字段。最后,我们点击对话框的 `确定` 按钮。此函数的代码主要从 tst_general 测试用例复制并修改。

def closeWithoutSaving():
    sendEvent("QCloseEvent", waitForObject(names.mainWindow));
    clickButton(waitForObject(names.address_Book_No_QPushButton))
function closeWithoutSaving()
{
    sendEvent("QCloseEvent", waitForObject(names.mainWindow));
    clickButton(waitForObject(names.addressBookNoQPushButton));
}
sub closeWithoutSaving
{
    sendEvent( "QCloseEvent", waitForObject($Names::mainwindow) );
    clickButton(waitForObject($Names::address_book_no_qpushbutton));
}
def closeWithoutSaving
    sendEvent("QCloseEvent", waitForObject(Names::MainWindow))
    clickButton(waitForObject(Names::Address_Book_No_QPushButton))
end
proc closeWithoutSaving {} {
    sendEvent QCloseEvent [waitForObject $names::MainWindow]
    invoke clickButton [waitForObject $names::Address_Book_No_QPushButton]
}

在这里,我们使用 sendEvent 函数来模拟关闭窗口。我们使用的真实名称 names.mainWindow 是我们手动创建的对象映射条目。它仅指定 type=MainWindow,因此它应该与我们的主窗口匹配,而不论窗口标题如何。

接下来,我们点击 `保存未保存的更改?` 对话框的 `` 按钮。最后一行是从录制的测试中复制的。

整个测试用例不到 30 行代码——如果我们把一些常见功能(如 invokeMenuItemcloseWithoutSaving)放在共享脚本中,行数会更少。并且大部分代码直接来自录制的测试,有些地方进行了参数化。

这应该足够了解如何为 AUT 编写测试脚本。请记住,Squish 提供的功能远不止这里使用的内容(所有这些内容都在 API 参考手册工具参考手册 中有所涵盖)。Squish 还提供了对 AUT 对象所有公共 API 的访问。

然而,测试案例的一个方面不太令人满意。虽然像这里一样嵌入测试数据对于少量数据是有道理的,但它相当有限,特别是当我们想要使用大量测试数据时。此外,我们没有测试任何我们添加的数据,以验证它是否正确地出现在 `QTableWidget` 中。在下一节中,我们将创建此测试的新版本,但这一次,我们将从外部数据源中提取数据,并检查我们添加到 `QTableWidget` 的数据是否正确。

创建数据驱动测试

在上一节中,我们将三个硬编码的名称和地址放入了我们的测试中。但如果我们想测试大量的数据呢?或者如果我们想在不必更改测试脚本源代码的情况下更改数据呢?一种方法是导入数据集到Squish中,并将数据集作为我们插入测试中的值的来源。Squish可以导入以下格式的数据:.tsv(制表符分隔值格式)、.csv(逗号分隔值格式)、.xls.xlsx(Microsoft Excel电子表格格式)。

注意:假设.csv.tsv文件都使用UTF-8编码,该编码用于所有测试脚本。

测试数据可以使用squishide导入,或者使用文件管理器或控制台命令手动导入。我们将描述这两种方法,首先是使用squishide

对于地址簿应用程序,我们想导入MyAddresses.tsv数据文件。为此,必须首先点击文件 > 导入测试资源以弹出导入Squish资源对话框。在对话框内部,单击浏览按钮以选择要导入的文件——在本例中为MyAddresses.tsv。确保将导入为组合框设置为“测试数据”。默认情况下,squishide只会导入当前测试用例的测试数据,但我们想让所有测试套件的测试用例都能使用测试数据:为此,选中复制到测试套件以共享单选按钮。现在单击完成按钮。您现在可以在测试套件资源视图(在测试数据选项卡中)中看到所列的文件,并且如果您单击文件名,它将在编辑视图中显示。截图中显示了添加测试数据后的Squish。

要从squishide外部导入测试数据,请使用文件管理器,例如文件资源管理器或查找器,或使用控制台命令。在测试套件目录中创建一个名为shared的目录。然后,在shared目录中创建一个名为testdata的目录。将数据文件(在本例中为MyAddresses.tsv)复制到shared\testdata目录中。

如果squishide正在运行,请重新启动。如果您单击测试套件资源视图的测试数据选项卡,您应该看到数据文件。单击文件名,您可以在编辑视图中查看文件。

"Squish with some imported test data"

尽管在实际生活中我们会修改我们的tst_adding测试用例以使用测试数据,但为了教程的目的,我们将创建一个新的测试用例,命名为tst_adding_data,它是tst_adding的副本,并且我们将对其进行修改,以便使用测试数据。

唯一需要更改的功能是main,在这里,我们不是遍历硬编码的数据项,而是遍历数据集中所有的记录。我们还需要更新最后的期望行数,因为我们现在要添加很多记录,我们还将添加一个验证每个添加的记录的功能。

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/addressbook/addressbook"')
    invokeMenuItem("File", "New")
    table = waitForObject({"type": "QTableWidget"})
    test.verify(table.rowCount == 0)
    limit = 10  # To avoid testing 100s of rows since that would be boring
    for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
        forename = testData.field(record, "Forename")
        surname = testData.field(record, "Surname")
        email = testData.field(record, "Email")
        phone = testData.field(record, "Phone")
        table.setCurrentCell(0, 0)  # always insert at the start
        addNameAndAddress((forename, surname, email, phone))  # pass as a single tuple
        checkNameAndAddress(table, record)
        if row > limit:
            break
    test.compare(table.rowCount, row + 1, "table contains as many rows as added data")
    closeWithoutSaving()
import * as names from 'names.js';

function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"type": "QMenuBar"}, menu));
    activateItem(waitForObjectItem({"title": menu, "type": "QMenu"}, item));
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/addressbook/addressbook\"");
    invokeMenuItem("File", "New");
    my $table = waitForObject({"type" => "QTableWidget"});
    test::verify($table->rowCount == 0);
    my $limit = 10; # To avoid testing 100s of rows since that would be boring
    my @records = testData::dataset("MyAddresses.tsv");
    my $row = 0;
    for (; $row < scalar(@records); ++$row) {
        my $record = $records[$row];
        my $forename = testData::field($record, "Forename");
        my $surname = testData::field($record, "Surname");
        my $email = testData::field($record, "Email");
        my $phone = testData::field($record, "Phone");
        $table->setCurrentCell(0, 0); # always insert at the start
        addNameAndAddress($forename, $surname, $email, $phone);
        checkNameAndAddress($table, $record);
        if ($row > $limit) {
            last;
        }
    }
    test::verify($table->rowCount == $row + 1);
    closeWithoutSaving();
}
require 'names'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/addressbook/addressbook\"")
    invokeMenuItem("File", "New")
    table = waitForObject({:type => "QTableWidget"})
    Test.verify(table.rowCount == 0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    rows = 0
    TestData.dataset("MyAddresses.tsv").each_with_index do
        |record, row|
        forename = TestData.field(record, "Forename")
        surname = TestData.field(record, "Surname")
        email = TestData.field(record, "Email")
        phone = TestData.field(record, "Phone")
        table.setCurrentCell(0, 0) # always insert at the start
        addNameAndAddress([forename, surname, email, phone]) # pass as a single Array
        checkNameAndAddress(table, record)
        break if row > limit
        rows += 1
    end
    Test.compare(table.rowCount, rows + 1, "table contains as many rows as added data")
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/addressbook/addressbook\""
    invokeMenuItem "File" "New"
    set table [waitForObject [::Squish::ObjectName type QTableWidget]]
    test compare [property get $table rowCount] 0
    # To avoid testing 100s of rows since that would be boring
    set limit 10
    set data [testData dataset "MyAddresses.tsv"]
    set columns [llength [testData fieldNames [lindex $data 0]]]
    set row 0
    for {} {$row < [llength $data]} {incr row} {
        set record [lindex $data $row]
        set forename [testData field $record "Forename"]
        set surname [testData field $record "Surname"]
        set email [testData field $record "Email"]
        set phone [testData field $record "Phone"]
        set details [list $forename $surname $email $phone]
        invoke $table setCurrentCell 0 0
        addNameAndAddress $details
        checkNameAndAddress $table $record
        if {$row > $limit} {
            break
        }
    }
    test compare [property get $table rowCount] [expr $row + 1]
    closeWithoutSaving
}

Squish通过其testData模块的功能提供对测试数据的访问——在这里,我们使用了Dataset testData.dataset(filename)函数来访问数据文件,并使其记录可用,以及String testData.field(record, fieldName)函数来检索每个记录的各个字段。

使用测试数据填充了QTableWidget之后,我们希望确保表中的数据与我们添加的数据相同,因此添加了checkNameAndAddress函数。我们还对比较的记录数量进行了限制,以使测试运行更快。

def checkNameAndAddress(table, record):
    for column in range(len(testData.fieldNames(record))):
        test.compare(table.item(0, column).text(),  # New addresses are inserted at the start
                     testData.field(record, column))
function checkNameAndAddress(table, record)
{
    for (var column = 0; column < testData.fieldNames(record).length; ++column)
        test.compare(table.item(0, column).text(), // New addresses are inserted at the start
                     testData.field(record, column));
}
sub checkNameAndAddress
{
    my($table, $record) = @_;
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        test::compare($table->item(0, $column)->text(), # New addresses are inserted at the start
                      testData::field($record, $column));
    }
}
def checkNameAndAddress(table, record)
    for column in 0...TestData.fieldNames(record).length
        Test.compare(table.item(0, column).text(),
                     TestData.field(record, column))
        # New addresses are inserted at the start
    end
end
proc checkNameAndAddress {table record} {
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set value [invoke [invoke $table item 0 $column] text]
        test compare $value [testData field $record $column]
    }
}

此函数访问QTableWidget的第一行,并提取其每列的值。我们使用Squish的SequenceOfStrings testData.fieldNames(record)函数获取列数,然后使用test.compare函数检查表中的每个值是否与我们使用的测试数据中的值相同。请注意,对于这次特定的测试,我们总是在表的开头插入新行。这种效果是每次新名称和地址总是作为第一行添加,因此我们硬编码该行为0。

截图显示了在运行数据驱动测试后Squish的测试摘要日志。

"Squish after a successful data-driven test run"

Squish还可以执行关键字驱动测试。这比数据驱动测试要复杂一些。请参阅如何进行关键字驱动测试

了解更多

现在我们已经完成了教程。Squish可以做很多更多我们在这里展示的内容,但目标是尽可能快地让您开始基本测试。有关如何创建测试脚本如何创建测试脚本和测试特定工具包的API如何测试应用程序 - 详细说明的部分提供了许多示例,包括显示测试如何与特定的输入元素相互作用,如选择框、单选按钮、文本和文本区域。

API参考API参考和工具参考工具参考提供了关于Squish的测试API和它提供的各种函数的完整详细信息,以使测试尽可能容易和高效。阅读如何创建测试脚本如何测试应用程序 - 详细说明以及浏览API参考API参考和工具参考工具参考是非常值得的。你投入的时间将得到回报,因为你将了解Squish提供的常规功能,并可以避免重新发明已存在的东西。

以下列出了关键Qt示例及其用途链接。

除了上面列出的文档示例之外,在 SQUISHDIR/examples/qt 中还提供了更多的 Qt 示例应用程序及其相应的测试。

教程:设计行为驱动开发(BDD)测试

本教程将向您展示如何为一个示例应用程序创建、运行和修改行为驱动开发(BDD)测试。您将了解 Squish 最常用的功能。在本教程结束时,您将能够为您的应用程序编写自己的测试。

本章中,我们将使用一个简单的地址簿应用程序作为我们的应用程序(AUT)。这是一个基本的应用程序,允许用户加载现有的地址簿或创建一个新的地址簿,添加、编辑和删除条目。截图中显示了用户在添加新名称和地址时应用程序的操作。

"The Qt addressbook example"

行为驱动开发(BDD)简介

行为驱动开发(BDD)是对测试驱动开发(TDD)方法的一种扩展,将验收标准的定义放在开发过程的开始,而不是在软件开发完成后编写测试。可能是在测试后进行代码更改的周期。

"BDD process"

行为驱动测试由一系列 Feature 文件构建而成,通过一个或多个 Scenario 描述产品功能。每个 Scenario 都由一系列步骤组成,这些步骤代表了要对该 Scenario 进行测试的动作或验证。

BDD 关注的是预期应用程序行为,而不是实现细节。因此,BDD 测试是以人类可读的域特定语言(DSL)描述的。由于这种语言是非技术的,这样的人工智能不仅能由程序员创建,还能由产品负责人、测试人员或业务分析师创建。此外,在产品开发过程中,此类测试还作为活的产品文档。对于 Squish 的使用,BDD 测试应使用 Gherkin 语法创建。先前编写的生产规范(BDD 测试)可以转换为可执行的测试。本逐步教程将展示使用 squishide 支持自动化的 BDD 测试。

Gherkin 语法

Gherkin 文件通过一个或多个场景描述应用程序预期的行为,以描述产品功能。以下是一个示例,展示了地址簿示例应用程序的“填充地址簿”功能。

Feature: Filling of addressbook
    As a user I want to fill the addressbook with entries

    Scenario: Initial state of created address book
        Given addressbook application is running
        When I create a new addressbook
        Then addressbook should have zero entries

    Scenario: State after adding one entry
        Given addressbook application is running
        When I create a new addressbook
        And I add a new person 'John','Doe','[email protected]','500600700' to address book
        Then '1' entries should be present

    Scenario: State after adding two entries
        Given addressbook application is running
        When I create a new addressbook
        And I add new persons to address book
            | forname   | surname  | email        | phone  |
            | John      | Smith    | john@m.com   | 123123 |
            | Alice     | Thomson  | alice@m.com  | 234234 |
        Then '2' entries should be present

    Scenario: Forename and surname is added to table
        Given addressbook application is running
        When I create a new addressbook
        When I add a new person 'Bob','Doe','[email protected]','123321231' to address book
        Then previously entered forename and surname shall be at the top

上述大部分是自由文本(不一定是英语)。只是 Feature/Scenario 结构和“Given”、“And”、“When”和“Then”等领先的关键词是固定的。这些关键词中的每一个都标记了一个步骤,该步骤定义了前提、用户动作和预期结果。上述应用行为描述可以传递给软件开发人员以实现这些功能,同时还可以将该描述传递给软件测试人员以实现自动化测试。

测试实现

创建测试套件

首先,我们需要创建一个测试套件,它是所有测试用例的容器。启动 squishide 并选择 文件 > 新建测试套件。按照新建测试套件向导,提供测试套件名称,选择您选择的 Qt 工具包和脚本语言,最后将地址簿应用程序注册为 AUT。有关创建新测试套件的更多详细信息,请参阅 创建测试套件

创建测试用例

Squish提供了两种测试用例类型:“脚本测试用例”和“BDD测试用例”。由于“脚本测试用例”是默认的,因此要创建新的“BDD测试用例”,需要使用右键点击位于新建测试用例按钮旁边的展开器并选择新建BDD测试用例选项。生成的squishide将记住您的选择,并且在将来点击按钮时,“BDD测试用例”将变为默认选项。

"Creating new BDD Test Case"

新创建的BDD测试用例包括一个test.feature文件(在创建新的BDD测试用例时用Gherkin模板填充),一个名为test.(py|js|pl|rb|tcl)的文件,该文件将驱动执行(无需编辑此文件),以及一个名为shared/steps/steps.(py|js|pl|rb|tcl)的测试套件资源文件,其中将放置步骤实现代码。

我们需要用Feature替换Gherkin模板,以便用于addressbook示例应用程序。为此,复制下面的Feature描述并粘贴到Feature文件中。

Feature: Filling of addressbook
    As a user I want to fill the addressbook with entries

    Scenario: Initial state of created address book
        Given addressbook application is running
        When I create a new addressbook
        Then addressbook should have zero entries

在编辑test.feature文件时,每个未定义的步骤都会显示一个Feature文件警告“未找到实现”。实现代码位于steps子目录中,在测试用例资源测试套件资源中。现在运行我们的Feature测试将会在第一个步骤失败,并显示“没有匹配的步骤定义”,接下来将跳过后续步骤。

录制步骤实现

为了录制Scenario,请按位于测试用例资源视图中“情景”标签下列出的相应Scenario旁边的记录按钮。

"Record Scenario"

这将使Squish运行AUT(应用程序运行时),以便我们可以与之交互。此外,将会显示控制栏,其中包括需要录制的所有步骤的列表。现在,与AUT的所有交互或添加到脚本的任何验证点都将被记录在第一步Given addressbook应用程序正在运行(在控制栏的步骤列表中加粗显示)下。为了验证该先决条件是否满足,我们将添加一个验证点。为此,点击控制栏中的验证并选择属性

"Control Bar"

因此,squishide被置于间谍模式下,显示了所有应用程序对象及其属性。在应用程序对象视图中,选择(不要勾选)addressbook的MainWindow项。选择它将更新其右侧的属性视图。接下来,点击属性视图前面的复选框。最后,点击保存并插入验证按钮。squishide消失,然后再显示控制栏

"Inserting Verification Point"

当完成每个步骤后,我们可以通过点击控制栏中位于当前步骤左侧的完成录制步骤()箭头按钮来移动到下一个未定义的步骤(回放之前定义的步骤)。

接下来,对于步骤当我在地址簿中创建一个新的地址簿时,点击地址簿应用程序工具栏中的新建按钮(),然后点击完成录制步骤()以进入下一个步骤。

最后,验证步骤Then addressbook should have zero entries时,确认包含地址条目的表格是否为空。为了记录这一验证,在录制时点击Verify,并选择Properties。在Application Objects中,通过导航或使用Object Picker)选择(不勾选)包含地址簿条目的表格对象(在我们的案例中,此表格为空)。然后,从Properties视图中检查rowCount属性,并点击Save and Insert Verifications。最后,点击控制栏中最后的一个Finish Recording Step)箭头按钮。

因此,Squish将在steps.*文件(位置在Test Suites > Test Suite Resources)中生成以下步骤定义:

@Given("addressbook application is running")
def step(context):
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/addressbook/addressbook"')
    test.compare(waitForObjectExists(names.address_Book_MainWindow).enabled, True)

@Step("I create a new addressbook")
def step(context):
    clickButton(waitForObject(names.address_Book_New_QToolButton))

@Then("addressbook should have zero entries")
def step(context):
    test.compare(waitForObjectExists(names.address_Book_Unnamed_File_QTableWidget).rowCount, 0)
Given("addressbook application is running") do |context|
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/addressbook/addressbook\"")
    Test.compare(waitForObjectExists(Names::Address_Book_MainWindow).enabled, true)
end

When("I create a new addressbook") do |context|
    clickButton(waitForObject(Names::Address_Book_New_QToolButton))
end

Then("addressbook should have zero entries") do |context|
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_File_QTableWidget).rowCount, 0)
end
Given("addressbook application is running", function(context) {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/qt/addressbook/addressbook"');
    test.compare(waitForObjectExists(names.addressBookMainWindow).enabled, true);
});

When("I create a new addressbook", function(context) {
    clickButton(waitForObject(names.addressBookNewQToolButton));
});

Then("addressbook should have zero entries", function(context) {
    test.compare(waitForObjectExists(names.addressBookUnnamedFileQTableWidget).rowCount, 0);
});
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/addressbook/addressbook\"");
    test::compare(waitForObjectExists($Names::address_book_mainwindow)->enabled, 1 );
});

When("I create a new addressbook", sub {
    my $context = shift;
    clickButton(waitForObject($Names::address_book_new_qtoolbutton) );
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::address_book_unnamed_file_qtablewidget)->rowCount,0);
});
Given "addressbook application is running" {context} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/addressbook/addressbook\""
    test compare [property get [waitForObjectExists $names::Address_Book_MainWindow] enabled] true
}

When "I create a new addressbook" {context} {
    invoke clickButton [waitForObject $names::Address_Book_New_QToolButton]
}

Then "addressbook should have zero entries" {context} {
    test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_File_QTableWidget] rowCount] 0
}

由于录制的startApplication()调用,应用程序在第一步的开始时自动启动。在每场景的结尾,调用OnScenarioEnd钩子,导致应用程序上下文调用detach()。因为AUT(应用上下文)是使用startApplication()启动的,这导致它终止。这个钩子函数在bdd_hooks.(py|js|pl|rb|tcl)文件中找到,这个文件位于Test Suite Resources视图的Scripts标签中。你可以在那里定义额外的钩子函数。有关所有可用钩子的列表,请参阅通过钩子在测试执行期间执行操作

@OnScenarioEnd
def OnScenarioEnd(context):
    for ctx in applicationContextList():
        ctx.detach()
OnScenarioEnd(function(context) {
    applicationContextList().forEach(function(ctx) { ctx.detach(); });
});
OnScenarioEnd(sub {
    foreach (applicationContextList()) {
        $_->detach();
    }
});
OnScenarioEnd do |context|
    applicationContextList().each { |ctx| ctx.detach() }
end
OnScenarioEnd {context} {
    foreach ctx [applicationContextList] {
        applicationContext $ctx detach
    }
}

步骤参数化

到目前为止,我们的步骤没有使用任何参数,所有值都是硬编码的。Squish有不同的参数类型,比如anyintegerword,这使得我们的步骤定义更加可重用。让我们向我们的Feature文件中添加一个新的Scenario,这将同时为测试数据和预期结果提供步骤参数。将以下部分复制到您的Feature文件中。

Scenario: State after adding one entry
    Given addressbook application is running
    When I create a new addressbook
    And I add a new person 'John','Doe','[email protected]','500600700' to address book
    Then '1' entries should be present

自动保存Feature文件后,squishide提供了一个提示,指出只需要实现2个步骤:When I add a new person 'John', 'Doe','[email protected]','500600700' to address bookThen '1' entries should be present。其余的步骤已经有相应的步骤实现。

要录制缺少的步骤,在测试套件视图中,点击测试用例名称旁边的记录按钮。脚本将播放,直到到达缺少的步骤,然后提示你实现它。如果你选择添加按钮,则可以输入新条目的信息。点击Finish Recording Step)以进入下一个步骤。对于第二个缺少的步骤,我们可以录制类似于步骤Then addressbook should have zero entries的对象属性验证。或者,我们可以在steps.(py|js|pl|rb|tcl)文件中复制该步骤的实现,并在test.compare行末递增数字。我们不是测试为零项目,而是测试为一个项目。

现在我们对生成的When步骤实现进行参数化,通过将值替换为参数类型。因为我们想能够添加不同的名称,所以将'John'替换为'|word|'。注意,每个参数将按步骤描述性名称中出现的顺序传递给步骤实现函数。通过将输入的值编辑为像以下示例步骤一样的关键词来完成参数化:When I add a new person 'John', 'Doe','[email protected]','500600700'

@When("I add a new person '|word|','|word|','|any|','|integer|' to address book")
def step(context, forename, surname, email, phone):
    clickButton(waitForObject(names.address_Book_Add_QToolButton))
    type(waitForObject(names.forename_LineEdit), forename)
    type(waitForObject(names.surname_LineEdit), surname)
    type(waitForObject(names.email_LineEdit), email)
    type(waitForObject(names.phone_LineEdit), phone)
    clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
When("I add a new person '|word|','|word|','|any|','|integer|' to address book",
    function (context, forename, surname, email, phone){
        clickButton(waitForObject(names.addressBookUnnamedAddQToolButton));
        type(waitForObject(names.forenameLineEdit), forename);
        type(waitForObject(names.surnameLineEdit), surname);
        type(waitForObject(names.emailLineEdit), email);
        type(waitForObject(names.phoneLineEdit), phone);
        clickButton(waitForObject(names.addressBookAddOKQPushButton));
        context.userData["forename"] = forename;
        context.userData["surname"] = surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub {
    my $context = shift;
    my ($forename, $surname, $email, $phone) = @_;
    clickButton(waitForObject($Names::address_book_unnamed_add_qtoolbutton));
    type(waitForObject($Names::forename_lineedit), $forename);
    type(waitForObject($Names::surname_lineedit),  $surname);
    type(waitForObject($Names::email_lineedit),    $email);
    type(waitForObject($Names::phone_lineedit),    $phone);
    clickButton(waitForObject($Names::address_book_add_ok_qpushbutton));
    $context->{userData}{'forename'} = $forename;
    $context->{userData}{'surname'} = $surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone|
    clickButton(waitForObject(Names::Address_Book_Unnamed_Add_QToolButton))
    type(waitForObject(Names::Forename_LineEdit), forename)
    type(waitForObject(Names::Surname_LineEdit), surname)
    type(waitForObject(Names::Email_LineEdit), email)
    type(waitForObject(Names::Phone_LineEdit), phone)
    clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
    context.userData = Hash.new
    context.userData[:forename] = forename
    context.userData[:surname] = surname
end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} {
    invoke clickButton [waitForObject $names::Address_Book_Unnamed_Add_QToolButton]
    invoke type [waitForObject $names::Forename_LineEdit] $forename
    invoke type [waitForObject $names::Surname_LineEdit] $surname
    invoke type [waitForObject $names::Email_LineEdit] $email
    invoke type [waitForObject $names::Phone_LineEdit] $phone
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]
    $context userData [dict create forename $forename surname $surname]
}

如果我们把最后的“Then”步骤记录为缺失步骤,并验证表中rowCount为1,我们可以修改这个步骤,使其接受一个参数,以便以后验证其他整数值。

@Then("'|integer|' entries should be present")
def step(context, num):
    snooze(.25);
    test.compare(waitForObjectExists(names.address_Book_Unnamed_File_QTableWidget).rowCount, num)
Then("'|integer|' entries should be present", function(context, rowCount) {
    snooze(0.25)
    test.compare(waitForObjectExists(names.addressBookUnnamedFileQTableWidget).rowCount, rowCount);
});
Then("'|integer|' entries should be present", sub {
    my $context = shift;
    my $num = shift;
    test::compare(waitForObjectExists($Names::address_book_unnamed_file_qtablewidget)->rowCount, $num);
});
Then("'|integer|' entries should be present") do |context, num|
    Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_File_QTableWidget).rowCount, num)
end
Then "'|integer|' entries should be present" {context num} {
   test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_File_QTableWidget] rowCount] $num
}

在表中提供步骤的参数

下一个“Scenario”将测试向通讯录添加多条记录。我们可以通过只是使用不同的数据多次使用步骤“When I add a new person John','Doe','[email protected]','500600700' to address book”,但让我们定义一个新的步骤“When I add a new person to address book”,它会处理表中的数据。

Scenario: State after adding two entries
    Given addressbook application is running
    When I create a new addressbook
    And I add new persons to address book
        | forename  | surname  | email        | phone  |
        | John      | Smith    | john@m.com   | 123123 |
        | Alice     | Thomson  | alice@m.com  | 234234 |
    Then '2' entries should be present

处理此类表的步骤实现看起来是这样的

@Step("I add new persons to address book")
def step(context):
    table = context.table
    # Drop initial row with column headers
    table.pop(0)
    for (forname, surname, email, phone) in table:
        clickButton(waitForObject(names.address_Book_Add_QToolButton))
        snooze(.5) # workaround for qt4
        type(waitForObject(names.forename_LineEdit), forname)
        type(waitForObject(names.surname_LineEdit), surname)
        type(waitForObject(names.email_LineEdit), email)
        type(waitForObject(names.phone_LineEdit), phone)
        clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
        test.log("Added entry: "+forname+","+surname+","+email+","+phone);
When("I add new persons to address book", function(context) {
    var table = context.table;
    // Drop initial row with column headers
    for (var i = 1; i < table.length; ++i) {
        var forename = table[i][0];
        var surname = table[i][1];
        var email = table[i][2];
        var phone = table[i][3];
        clickButton(waitForObject(names.addressBookUnnamedAddQToolButton))
        snooze(0.5) // workaround for qt4
        type(waitForObject(names.forenameLineEdit), forename)
        type(waitForObject(names.surnameLineEdit), surname)
        type(waitForObject(names.emailLineEdit), email)
        type(waitForObject(names.phoneLineEdit), phone)
        clickButton(waitForObject(names.addressBookAddOKQPushButton))
    }
});
When("I add new persons to address book", sub {
    my %context = %{shift()};
    my @table = @{$context{'table'}};
    # Drop initial row with column headers
    shift(@table);
    for my $row (@table) {
        my ($forename, $surname, $email, $phone) = @{$row};
        clickButton( waitForObject($Names::address_book_unnamed_add_qtoolbutton) );
        snooze(0.5); # workaround for qt4
        type( waitForObject($Names::forename_lineedit), $forename );
        type( waitForObject($Names::surname_lineedit),  $surname );
        type( waitForObject($Names::email_lineedit),    $email );
        type( waitForObject($Names::phone_lineedit),    $phone );
        clickButton( waitForObject($Names::address_book_add_ok_qpushbutton) );
    }
});
When("I add new persons to address book") do |context|
    table = context.table
    # Drop initial row with column headers
    table.shift
    for forename, surname, email, phone in table do
        clickButton(waitForObject(Names::Address_Book_Unnamed_Add_QToolButton))
        snooze(0.5) # qt4 workaround
        type(waitForObject(Names::Forename_LineEdit), forename)
        type(waitForObject(Names::Surname_LineEdit), surname)
        type(waitForObject(Names::Email_LineEdit), email)
        type(waitForObject(Names::Phone_LineEdit), phone)
        clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
    end
end
When "I add new persons to address book" {context} {
    set table [$context table]
    # Drop initial row with column headers
    foreach row [lreplace $table 0 0] {
        foreach {forename surname email phone} $row break
        invoke clickButton [waitForObject $names::Address_Book_Unnamed_Add_QToolButton]
        # qt4 workaround:
        snooze 0.5
        invoke type [waitForObject $names::Forename_LineEdit] $forename
        invoke type [waitForObject $names::Surname_LineEdit] $surname
        invoke type [waitForObject $names::Email_LineEdit] $email
        invoke type [waitForObject $names::Phone_LineEdit] $phone
        invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]
    }
}

步骤和场景之间的数据共享

让我们向Feature文件添加一个新“Scenario”。这次我们想检查的不仅是对通讯录列表中的条目数量,而且还检查列表中是否包含正确的数据。因为我们在一个步骤中输入数据,并在另一个步骤中验证它们,所以我们必须在这些步骤间共享输入数据的信息,以便执行验证。

Scenario: Forename and surname is added to table
    Given addressbook application is running
    When I create a new addressbook
    When I add a new person 'Bob','Doe','[email protected]','123321231' to address book
    Then previously entered forename and surname shall be at the top

为了共享这些数据,可以使用context.userData属性。

@When("I add a new person '|word|','|word|','|any|','|integer|' to address book")
def step(context, forename, surname, email, phone):
    clickButton(waitForObject(names.address_Book_Add_QToolButton))
    type(waitForObject(names.forename_LineEdit), forename)
    type(waitForObject(names.surname_LineEdit), surname)
    type(waitForObject(names.email_LineEdit), email)
    type(waitForObject(names.phone_LineEdit), phone)
    clickButton(waitForObject(names.address_Book_Add_OK_QPushButton))
    context.userData = {}
    context.userData['forename'] = forename
    context.userData['surname'] = surname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book",
    function (context, forename, surname, email, phone){
        clickButton(waitForObject(names.addressBookUnnamedAddQToolButton));
        type(waitForObject(names.forenameLineEdit), forename);
        type(waitForObject(names.surnameLineEdit), surname);
        type(waitForObject(names.emailLineEdit), email);
        type(waitForObject(names.phoneLineEdit), phone);
        clickButton(waitForObject(names.addressBookAddOKQPushButton));
        context.userData["forename"] = forename;
        context.userData["surname"] = surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub {
    my $context = shift;
    my ($forename, $surname, $email, $phone) = @_;
    clickButton(waitForObject($Names::address_book_unnamed_add_qtoolbutton));
    type(waitForObject($Names::forename_lineedit), $forename);
    type(waitForObject($Names::surname_lineedit),  $surname);
    type(waitForObject($Names::email_lineedit),    $email);
    type(waitForObject($Names::phone_lineedit),    $phone);
    clickButton(waitForObject($Names::address_book_add_ok_qpushbutton));
    $context->{userData}{'forename'} = $forename;
    $context->{userData}{'surname'} = $surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone|
    clickButton(waitForObject(Names::Address_Book_Unnamed_Add_QToolButton))
    type(waitForObject(Names::Forename_LineEdit), forename)
    type(waitForObject(Names::Surname_LineEdit), surname)
    type(waitForObject(Names::Email_LineEdit), email)
    type(waitForObject(Names::Phone_LineEdit), phone)
    clickButton(waitForObject(Names::Address_Book_Add_OK_QPushButton))
    context.userData = Hash.new
    context.userData[:forename] = forename
    context.userData[:surname] = surname
end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} {
    invoke clickButton [waitForObject $names::Address_Book_Unnamed_Add_QToolButton]
    invoke type [waitForObject $names::Forename_LineEdit] $forename
    invoke type [waitForObject $names::Surname_LineEdit] $surname
    invoke type [waitForObject $names::Email_LineEdit] $email
    invoke type [waitForObject $names::Phone_LineEdit] $phone
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_QPushButton]
    $context userData [dict create forename $forename surname $surname]
}

在给定Feature的所有步骤和所有Hooks中,可以访问在context.userData中存储的所有数据。最后,我们需要实现步骤“Then previously entered forename and lastname shall be at the top”。

@Then("previously entered forename and surname shall be at the top")
def step(context):
    test.compare(waitForObjectExists(names.file_0_0_QModelIndex).text,context.userData['forename']);
    test.compare(waitForObjectExists(names.file_0_1_QModelIndex).text,context.userData['surname']);
Then("previously entered forename and surname shall be at the top",function(context){
    test.compare(waitForObjectExists(names.file00QModelIndex).text,context.userData["forename"], "forname?");
    test.compare(waitForObjectExists(names.file01QModelIndex).text,context.userData["surname"], "surname?");
});
Then("previously entered forename and surname shall be at the top", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::file_0_0_qmodelindex)->text,  $context->{userData}{'forename'}, "forename?" );
    test::compare(waitForObjectExists($Names::file_0_1_qmodelindex)->text,  $context->{userData}{'surname'}, "surname?" );
});
Then("previously entered forename and surname shall be at the top") do |context|
    Test.compare(waitForObjectExists(Names::File_0_0_QModelIndex).text, context.userData[:forename], "forename?")
    Test.compare(waitForObjectExists(Names::File_0_1_QModelIndex).text, context.userData[:surname], "surname?")
end
Then "previously entered forename and surname shall be at the top" {context} {
    test compare [property get [waitForObjectExists $names::File_0_0_QModelIndex] text] [dict get [$context userData] forename]
    test compare [property get [waitForObjectExists $names::File_0_1_QModelIndex] text] [dict get [$context userData] surname]
}

通过记录一个验证第0行、第0列和第1列的单元格文本属性的片段,我们得到了脚本化的验证点在QModelIndex对象上。我们在片段中替换了实际的值,使用从上一个步骤存储的context.userData值。

场景概述

假设我们的Feature包含以下两个Scenario

Scenario: State after adding one entry
    Given addressbook application is running
    When I create a new addressbook
    And I add a new person 'John','Doe','[email protected]','500600700' to address book
    Then '1' entries should be present

Scenario: State after adding one entry
    Given addressbook application is running
    When I create a new addressbook
    And I add a new person 'Bob','Koo','[email protected]','500600800' to address book
    Then '1' entries should be present

正如我们所看到的,这些Scenario使用不同的测试数据进行相同的行为。通过使用Scenario Outline(一个带有占位符的Scenario模板)和示例(一个包含参数的表)也可以实现同样的效果。

Scenario Outline: Adding single entries multiple time
   Given addressbook application is running
   When I create a new addressbook
   And I add a new person '<forename>','<lastname>','<email>','<phone>' to address book
   Then '1' entries should be present
   Examples:
      | forename | lastname | email       | phone     |
      | John     | Doe      | john@m.com  | 500600700 |
      | Bob      | Koo      | bob@m.com   | 500600800 |

请注意,在Scenario Outline中,每次迭代结束时都会执行OnScenarioEnd钩子。

测试执行

squishide中,用户可以执行一个Feature中的所有Scenarios,或者只执行一个选定的Scenario。为了执行所有Scenarios,必须通过单击测试套件视图中的Play按钮来执行适当的测试用例。

"Execute all Scenarios from Feature"

为了仅执行一个Scenario,您需要打开Feature文件,右键单击给定的Scenario并选择Run Scenario。另一种方法是单击Scenarios选项卡中相应的Scenario旁边的Play按钮。

"Execute one Scenario from Feature"

Scenario执行后,根据执行结果对Feature文件进行着色。更详细的信息(如日志)可以在测试结果视图中找到。

"Execution results in Feature file"

测试调试

ontwikkelaar biedt de mogelijkheid om een test مورد在任何时刻停止执行以便检查脚本变量、监视应用程序对象或运行自定义代码在Squish Script Console。为此, moet een onderbreking worden geplaatst voordat de uitvoering begint,in het Feature-bestand op elke regel met een stap of op elke regel van uitgevoerde code (d.w.z. in het midden van de definities van stappen).

"Breakpoint in Feature file"

Naar aanleiding van de bereikte onderbreking, kunt u alle objecten van de applicatie en hun eigenschappen inspecteren. Als een onderbreking wordt geplaatst op een definitie van een stap of een hook, dan kunt u ook Verificatiepunten toevoegen of codefragmenten opnemen.

Hergebruik van definities van stappen

De bewerkbaarheid van Tests Based on Behavior-Driven Development (BDD) kan worden verhoogd door definities van stappen te hergebruiken in testgevallen die zich in een anderemap bevinden. Voor meer informatie, zie collectStepDefinitions().

Tutorial: Overgang van bestaande tests naar BDD

Dit hoofdstuk is geschreven voor gebruikers die bestaande Squish-tests hebben en die gedachten willen opvatten over Behavior-Driven Testing. De eerste sectie beschrijft hoe u de bestaande tests kunt behouden en alleen nieuwe tests maakt met de aanpak van BDD. De tweede sectie beschrijft hoe Script Testgevallen kunt omzetten in BDD-tests.

Extend existing tests to BDD

De eerste optie is om alle bestaande Squish-tests te behouden en deze te verlengen door nieuwe BDD-tests toe te voegen. Het is mogelijk om een Test Suite te hebben die zowel Script Testgevallen als BDD Testgevallen bevat. Open eenvoudig de bestaandeTest Suite met de testgevallen en kies de optie New BDD Test Case uit het drop-downmenu.

"Creating new BDD Test Case"

Aansluitend op de aanneming dat uw bestaande Testgevallen gebruik maken van een bibliotheek en u gedeelde functies aanroept om met de AUT te interageren, kunnen deze functies ook worden gebruikt in nieuw gemaakte BDD Testgevallen. In het onderstaande voorbeeld wordt een functie gebruikt in meerdere Script Testgevallen

def createNewAddressBook():
    clickButton(waitForObject(":Address Book.New_QToolButton"))
function createNewAddressBook(){
    clickButton(waitForObject(":Address Book.New_QToolButton"));
}
sub createNewAddressBook{
    clickButton(waitForObject(":Address Book.New_QToolButton"));
}
def createNewAddressBook
  clickButton(waitForObject(":Address Book.New_QToolButton"))
end
proc createNewAddressBook {} {
    invoke clickButton [waitForObject ":Address Book.New_QToolButton"]
}

Nieuwe BDD Testgevallen kunnen deze functie gemakkelijk gebruiken

@When("I create a new addressbook")
def step(context):
    createNewAddressBook()
When("I create a new addressbook",function(context){
    createNewAddressBook()
});
When("I create a new addressbook", sub {
    createNewAddressBook();
});
When("I create a new addressbook") do |context|
  createNewAddressBook
end
When "I create a new addressbook" {context} {
    createNewAddressBook
}

Convert existing tests to BDD

De tweede optie is om een bestaandeTest Suite die Script Testgevallen bevat, om te zetten in behavior driven tests. Omdat eenTest Suite zowel Script Testgevallen als BDD Testgevallen kan bevatten, kan migratie stap voor stap worden uitgevoerd. EenTest Suite die een mengsel van beide Testgevalstypen bevat, kan worden uitgevoerd en de resultaten kunnen worden geanalyseerd zonder extra inspanningen.

De eerste stap is om alle Testgevallen van de bestaandeTest Suite te bekijken en deze te organiseren op basis van hetFeature dat wordt getest. Elk Script Testgeval zal worden omgezet in eenScenario wat een deel van eenFeature is. Bijvoorbeeld, aannemend dat we 5 Script Testgevallen hebben. Na de beoordeling merken we dat deze Script Testgevallen tweeFeatures testen. Daarom zal de Test Suite, wanneer migratie is voltooid, twee BDD Testgevallen bevatten, elk van hen met éénFeature. ElkeFeature zal meerdereScenarios bevatten. In ons voorbeeld bevat de eersteFeature drieScenarios en de tweedeFeature tweeScenarios.

"Conversion Chart"

首先,在包含计划迁移到BDD测试的脚本Squish测试的“squishide”中打开一个测试套件。接下来,通过选择其下拉菜单中的新BDD测试用例选项来创建一个新测试用例。每个BDD测试用例包含一个test.feature文件,可以填写一个最大的功能。接下来,打开test.feature文件,使用Gherkin语言描述特性。按照模板中的语法,编辑功能名称,并提供可选的简短描述。接下来,分析脚本测试用例中执行的动作和验证,这些动作和验证需要迁移。以下是一个对于地址簿应用示例测试用例的例子。

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/qt/addressbook/addressbook"')
    invokeMenuItem("File", "New")
    table = waitForObject({"type": "QTableWidget"})
    test.verify(table.rowCount == 0)
    data = [("Andy", "Beach", "[email protected]", "555 123 6786"),
            ("Candy", "Deane", "[email protected]", "555 234 8765"),
            ("Ed", "Fernleaf", "[email protected]", "555 876 4654")]
    for oneNameAndAddress in data:
        addNameAndAddress(oneNameAndAddress)
    waitForObject(table);
    test.compare(table.rowCount, len(data), "table contains as many rows as added data")
    closeWithoutSaving()
import * as names from 'names.js';

function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"type": "QMenuBar"}, menu));
    activateItem(waitForObjectItem({"title": menu, "type": "QMenu"}, item));
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/qt/addressbook/addressbook\"");
    invokeMenuItem("File", "New");
    my $table = waitForObject({"type" => "QTableWidget"});
    test::verify($table->rowCount == 0);
    my @data = (["Andy", "Beach", "andy.beach\@nowhere.com", "555 123 6786"],
                ["Candy", "Deane", "candy.deane\@nowhere.com", "555 234 8765"],
                ["Ed", "Fernleaf", "ed.fernleaf\@nowhere.com", "555 876 4654"]);
    foreach my $oneNameAndAddress (@data) {
        addNameAndAddress(@{$oneNameAndAddress});
    }
    test::compare($table->rowCount, scalar(@data), "table contains as many rows as added data");
    closeWithoutSaving();
}
require 'names'
include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/qt/addressbook/addressbook\"")
    invokeMenuItem("File", "New")
    table = waitForObject({:type => "QTableWidget"})
    Test.verify(table.rowCount == 0)
    data = [["Andy", "Beach", "[email protected]", "555 123 6786"],
        ["Candy", "Deane", "[email protected]", "555 234 8765"],
        ["Ed", "Fernleaf", "[email protected]", "555 876 4654"]]
    data.each do |oneNameAndAddress|
        addNameAndAddress(oneNameAndAddress)
    end
    Test.compare(table.rowCount, data.length, "table contains as many rows as added data")
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/qt/addressbook/addressbook\""
    set table [waitForObject [::Squish::ObjectName type QTableWidget]]
    test compare [property get $table rowCount] 0
    invokeMenuItem "File" "New"
    set data [list \
        [list "Andy" "Beach" "[email protected]" "555 123 6786"] \
        [list "Candy" "Deane" "[email protected]" "555 234 8765"] \
        [list "Ed" "Fernleaf" "[email protected]" "555 876 4654"] ]
    for {set i 0} {$i < [llength $data]} {incr i} {
        addNameAndAddress [lindex $data $i]
    }
    waitForObject $table
    test compare [property get $table rowCount] [llength $data] "table contains as many rows as added data"
    closeWithoutSaving
}

分析上述脚本测试用例后,我们可以创建以下场景,包括前三个步骤。

Scenario: Initial state of created address book
   Given addressbook application is running
   When I create a new addressbook
   Then addressbook should have zero entries

接下来,右键单击场景,并从上下文菜单中选择创建缺失步骤实现选项。这将创建步骤定义的骨架。

@Given("addressbook application is running")
def step(context):
    test.warning("TODO implement addressbook application is running")

@When("I create a new addressbook")
def step(context):
    test.warning("TODO implement I create a new addressbook")

@Then("addressbook should have zero entries")
def step(context):
    test.warning("TODO implement addressbook should have zero entries")
Given("addressbook application is running", function(context) {
    test.warning("TODO implement addressbook application is running");
});

When("I create a new addressbook", function(context) {
    test.warning("TODO implement I create a new addressbook");
});

Then("addressbook should have zero entries", function(context) {
    test.warning("TODO implement addressbook should have zero entries");
});
Given("addressbook application is running", sub {
    my $context = shift;
    test::warning("TODO implement addressbook application is running");
});

When("I create a new addressbook", sub {
    my $context = shift;
    test::warning("TODO implement I create a new addressbook");
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::warning("TODO implement addressbook should have zero entries");
});
Given("addressbook application is running") do |context|
    Test.warning "TODO implement addressbook application is running"
end

When("I create a new addressbook") do |context|
    Test.warning "TODO implement I create a new addressbook"
end

Then("addressbook should have zero entries") do |context|
    Test.warning "TODO implement addressbook should have zero entries"
end
Given "addressbook application is running" {context} {
    test warning "TODO implement addressbook application is running"
}

When "I create a new addressbook" {context} {
    test warning "TODO implement I create a new addressbook"
}

Then "addressbook should have zero entries" {context} {
    test warning "TODO implement addressbook should have zero entries"
}

现在我们将脚本测试用例中的代码片段放入相应的步骤定义中,并删除包含test.warning的行。如果您的脚本测试用例使用了共享脚本,您也可以从步骤定义中调用这些函数。例如,最终结果可能如下所示。

@Given("addressbook application is running")
def step(context):
    startApplication("addressbook")

@When("I create a new addressbook")
def step(context):
    invokeMenuItem("File", "New")

@Then("addressbook should have zero entries")
def step(context):
    table = waitForObject({"type": "QTableWidget"})
    test.compare(table.rowCount, 0)
Given("addressbook application is running", function(context) {
    startApplication("addressbook");
});

When("I create a new addressbook", function(context) {
    invokeMenuItem("File", "New");
});

Then("addressbook should have zero entries", function(context) {
    var table = waitForObject({"type": "QTableWidget"});
    test.compare(table.rowCount, 0);
});
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("addressbook");
});

When("I create a new addressbook", sub {
    my $context = shift;
    invokeMenuItem("File", "New");
});

Then("addressbook should have zero entries", sub {
    my $table = waitForObject({"type" => "QTableWidget"});
    test::compare($table->rowCount, 0);
});
Given("addressbook application is running") do |context|
    startApplication("addressbook")
end

When("I create a new addressbook") do |context|
    invokeMenuItem("File", "New")
end

Then("addressbook should have zero entries") do |context|
    table = waitForObject({:type => "QTableWidget"})
    Test.compare(table.rowCount, 0)
end
Given "addressbook application is running" {context} {
    startApplication "addressbook"
}

When "I create a new addressbook" {context} {
    invokeMenuItem "File" "New"
}

Then "addressbook should have zero entries" {context} {
    set table [waitForObject [::Squish::ObjectName type QTableWidget]]
    test compare [property get $table rowCount] 0
}

请注意,在将此脚本测试用例迁移到BDD时,移除了test.log("Create new addressbook")。当执行步骤我创建一个新的地址簿时,步骤名称将记录在测试结果中,因此调用test.log是多余的。

在您的套件中创建了第一个BDD测试用例后,将在正确的脚本语言中创建一个shared/scripts/hooks文件。在squishide中,它位于测试套件资源脚本选项卡中。最初,这些文件包含一个在每次场景结束时调用的OnScenarioEnd钩子,以尝试确保AUT已被终止。

@OnScenarioEnd
def hook(context):
    for ctx in applicationContextList():
        ctx.detach()
OnScenarioEnd(function(context) {
    applicationContextList().forEach(function(ctx) { ctx.detach(); });
});
OnScenarioEnd(sub {
    foreach (applicationContextList()) {
        $_->detach();
    }
});
OnScenarioEnd do |context|
    applicationContextList().each { |ctx| ctx.detach() }
end
OnScenarioEnd {context} {
    foreach ctx [applicationContextList] {
        applicationContext $ctx detach
    }
}

上述示例为了教程简化。为了充分利用Squish中的行为驱动测试,请熟悉行为驱动测试部分,在API参考中。

教程:GUI元素覆盖率

GUI覆盖率是程序验证的一种形式,它测试测试套件中程序的所有GUI元素是否被激活。Squish现在支持Qt Widgets和QtQuick2元素的GUI覆盖率。不支持QtQuick1(也称为QtDeclarative)。本节描述Squish GUI覆盖率如何激活和使用。

激活

默认情况下,GUI覆盖率是关闭的。要激活它,将环境变量SQUISH_GUI_COVERAGE设置为1。使用其他任何值,GUI覆盖率将关闭。

squishide中,可以为使用startApplication()启动的AUT在测试套件中激活GUI覆盖率。从测试套件设置,选择AUT选项卡。然后输入变量SQUISH_GUI_COVERAGE环境表,并设置其值为1。

"Setting the environment variable"

从命令行激活

要在命令行上使用启用GUI覆盖率的Squish运行,在shell中将环境变量SQUISH_GUI_COVERAGE设置。然后可以直接以以下形式运行squishserver

$ export SQUISH_GUI_COVERAGE=1
$ squish/bin/squishserver

浏览覆盖率结果

测试用例可以像往常一样运行,但现在在测试结果窗口的最后多了一个条目,标题为GUI覆盖率结果。在信息列中显示包含测试结果的XML文件的位置,这有助于调试。

"Coverage results"

双击该条目,UI浏览器启动,并显示覆盖率结果。

"The UI browser"

UI浏览器显示有五个窗口

  • 元素树(左上角) – 显示元素及其覆盖率的层次结构。

    GUI覆盖率可以用“3/4 75%”这样的术语表示。这样的条目表示使用GUI元素完成了4个要求活动中3个,占所有活动的75%。在复杂的GUI元素(如窗口或菜单)中,这些数字指的是该元素及其包含的元素。在元素树中,元素的子元素将按照层次结构显示在该元素下方。

  • 屏幕截图(左下角) – 显示所选元素。
  • 元素属性(右上角) – 此窗口显示了所选GUI元素的静态属性。
    • class是用于实现元素的Qt类。
    • type是一个抽象类型,它描述了元素,与所使用的GUI工具无关。
  • 使用情况(中间右) – 列出了所选元素及其子元素上已注册的事件。

    列表中的每个条目都对应一个使用计数器。每个计数器都指代元素的一定活动。例如,“显示”计数器在每次显示元素时递增,而“点击”计数器在点击元素时递增。每种GUI元素都有自己的必须事件列表,“点击”例如属于菜单项,但不属于菜单。

  • 样式检查(右下角) – 如果元素的设计不遵循某些样式规则,则在此窗口中显示警告。

从命令行浏览结果

当squishrunner结束时,它将结果文件的位置以类似以下形式打印出来:

2015-07-07T15:30:55  ATTACHMENT GUI Coverage Result \
    addressbook/suite_py/tst_development/coverage_200790021.xml

然后您可以直接调用uibrowser来查看此文件的内容

$ squish/bin/uibrowser addressbook/suite_py/tst_development/coverage_200790021.xml

有关此命令的完整用法,请参阅uibrowser

配置

可以通过更改XML文件中的设置来配置GUI覆盖率,该文件的路径为SQUISHDIR/lib/qt/extensions/squishqtuicoverage.ext。该文件有三个条目,可以将它们设置为0或1以禁用或启用相应的功能。默认情况下,所有条目都被启用。

  • enable – 启用或禁用整体GUI覆盖率。
  • qtwidgets – 启用或禁用Qt小部件的覆盖。
  • qtquick – 启用或禁用Qt Quick的覆盖。
  • takeScreenshotsOfElements – 启用或禁用元素的屏幕截图。如果遇到内存问题或对这些元素不感兴趣,请禁用此选项。

如果存在问题,可以使用qtwidgetsqtquick部分禁用GUI覆盖率。但是,不推荐使用enable;请改用SQUISH_GUI_COVERAGE

©2024 The Qt Company Ltd. 所提供的文档贡献均为各自所有权者的版权。
在此提供的文档根据自由软件基金会发布的<地r href="http://www.gnu.org/licenses/fdl.html">GNU自由文档许可证版本1.3的条款进行许可。
Qt及相关标志是芬兰和/或世界其他国家的The Qt Company Ltd. 的商标。所有其他商标均为其各自所有权者的财产。