Squish for Windows 教程
了解如何测试本机 Windows 应用程序。
教程:开始测试Windows应用程序
Squish自带IDE和命令行工具。使用squishide
是最简单最有效的方法,但当你建立了很多测试之后,你将需要自动化它们。例如,进行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或shell脚本中运行的命令行工具是值得的。
注意:如果你需要视频指导,可以在上的45分钟在线课程Squish基本用法中找到。
对于本章,我们将使用一个简单的地址簿应用程序作为我们的AUT。该应用程序包含在Squish的SQUISHDIR/examples/win/Addressbook
中。
注意:在本手册中,我们经常引用SQUISHDIR
目录。这意味着安装Squish的目录,可能是C:\Squish
,/usr/local/squish
,/opt/local/squish
或其他,取决于你的安装位置。具体位置不重要,只要你将心理上的SQUISHDIR
目录理解为当你在此手册中看到路径和文件名时的实际目录。
地址簿应用程序是一个非常基础的程序,它允许用户加载现有的地址簿或创建一个新的,添加、编辑、删除条目,并将新修改的地址簿保存(或另存为)。尽管应用程序比较简单,但它包含了许多大多数标准应用程序都具有的关键功能:带下拉菜单的菜单栏、工具栏,以及中央区域——在这个例子中显示一个表格。它支持原地编辑,并且还有一个模态对话框用于添加项目。你学习测试这个应用程序的所有的想法和实践可以很容易地适应你自己的应用程序。有关测试各种Windows特性和标准编辑小部件的更多示例,请参阅如何创建测试脚本和如何测试Windows应用程序。
截图显示了用户添加新名字和地址时的应用程序操作。
使用示例
第一次尝试运行示例AUT之一测试时,你可能得到一个致命错误,开始于Squish无法找到要启动的AUT...。要恢复错误,单击测试套件设置工具栏按钮,然后在下方的应用程序(AUT)部分,如果可用,从组合框中选择AUT,或者单击浏览按钮,在查找器对话框中导航到AUT的可执行文件。某些版本的Squish如果没有指定AUT自动打开此对话框。你只需要对所有示例AUT进行一次这样做,测试你自己的AUT时不必这样做。
注意:根据你的安全设置,Windows可能会弹出一个对话框,询问你是否想要允许或阻止squishserver和/或你的AUT运行。如果你得到这个对话框,你必须选择阻止,以便Squish能够正常工作。
Squish概念
在下面的部分中,我们将创建一个测试套件,然后创建一些测试,但首先我们将简要回顾一些关键Squish概念。
进行测试,你需要
- 一个要测试的应用程序,称为应用程序(AUT)。
- 一个测试脚本,用于运行AUT。
Squish的实现方式有一个基本的方面,那就是被测应用(AUT)和用于测试的脚本总是在两个独立的进程中执行。这确保了即使AUT崩溃,也不会影响Squish。在这种情况下,测试脚本将优雅地失败并记录错误信息。将AUT和测试脚本在单独的进程中运行,除了隔离Squish和测试脚本免受AUT崩溃的影响外,还有其他好处。例如,这使得将测试脚本存储在中央位置以及在不同机器和平台上进行远程测试变得更加容易。进行远程测试的能力对于同时在多个平台上运行的被测应用(AUT)以及运行在嵌入式设备上的AUT尤其有用。
Squish运行一个小的服务器,squishserver,用于处理AUT和测试脚本之间的通信。测试脚本由squishrunner工具执行,该工具然后连接到squishserver。squishserver在设备上启动已仪化的AUT,它启动Squish钩子。钩子是一个小型库,它可以访问AUT的实时运行对象,并允许与squishserver通信。在放置了钩子之后,squishserver可以查询AUT对象的当前状态,并代表squishrunner执行命令。squishrunner指导AUT执行测试脚本指定的任何操作。
所有的通信都通过网络套接字进行,这意味着所有操作都可以在单一机器上执行,或者测试脚本可以在一台机器上执行,而AUT则可以在另一台机器上通过网络进行测试。
下面的图示说明了单个Squish工具如何协同工作。
从测试工程师的角度来看,这种分离是看不见的,因为所有通信都是在幕后透明处理。
可以使用squishide
编写和执行测试,在这种情况下,squishserver会自动启动和停止,测试结果会在squishide
的测试结果视图中显示。下面的图示说明了当使用squishide
时幕后发生了什么。
Squish工具也可以在命令行中使用而不需要squishide
。如果您更喜欢使用自己的工具,比如您最喜欢的编辑器或想要进行自动批测试,这很有用。例如,当在夜间运行回归测试时。在这些情况下,必须手动启动squishserver,并在所有测试完成后停止,或者为每个测试启动和停止。
注意: Squish文档主要使用术语小部件来指代GUI对象,如按钮、菜单、菜单项、标签和表格控件。Windows用户可能更熟悉术语控件和容器,但在这里我们对两者都使用术语小部件。类似地,macOS用户可能习惯使用术语视图。
制作可测试的应用程序
在大多数情况下,为了使应用程序可测试,不需要做任何特殊的事情,因为工具包的API(例如Qt)提供了足够的功能来实现和录制测试脚本。当squishide
启动AUT时,也会自动建立与squishserver的连接。
创建测试套件
测试套件是一组一个或多个测试用例(测试)。使用测试套件很方便,因为它使得在相关测试之间共享脚本和测试数据变得容易。
在本教程中,我们将首先介绍如何使用 squishide
来操作,然后针对命令行用户提供相关信息。
从 squishide
创建测试套件
启动 squishide
,通过点击或双击 squishide 图标,从任务栏菜单中启动 squishide,或者在命令行上运行 squishide,您可以选择最喜欢的、适合您所使用平台的方式来启动它。Squish 启动后,您可能会看到一个 欢迎页面。点击右上角的 工作台 按钮以关闭它。然后,squishide 将看起来与屏幕截图类似,但可能根据您使用的窗口系统、颜色、字体和主题略有不同。
Squish 启动后,点击 文件 > 创建新测试套件,将弹出以下所示的 创建新 Squish 测试用例向导。
输入测试套件的名称并选择您希望存储测试套件的文件夹。在屏幕截图中,我们将测试套件命名为 suite_py
并将其放入 Addressbook
文件夹中。(对于您自己的测试,您可能希望使用更具有意义的名称,例如 "suite_addressbook";我们选择了 "suite_py",因为我们的教程提供了几个套件,每个套件针对 Squish 支持的每种脚本语言。)当然,您可以选择您喜欢的任何名称和文件夹。完成详细信息后,点击 下一步 以进入工具包(或脚本语言)页面。
如果您获得此向导页面,请选择您的 AUT 使用的工具包。在本例中,我们必须点击 Windows,因为我们正在测试一个本地 Windows 应用程序。然后点击 下一步 以进入脚本语言页面。
选择您想要的任何脚本语言——唯一的限制是每个测试套件只能使用一种脚本语言。(所以如果您想使用多种脚本语言,只需创建多个测试套件,每个套件针对您想使用的每种脚本语言。)Squish 为所有语言提供的功能相同。如果您是脚本语言的初学者,Python 最容易学习和使用,并且可以说是阅读性和维护性最好的。选择脚本语言后,再次点击 下一步 以进入向导的最后页面。
如果您正在为 Squish 已知的 AUT 创建新的测试套件,只需点击组合框展开 AUT 列表,然后选择您想要的即可。如果组合框为空或您的 AUT 未列出,请点击组合框右侧的 浏览 按钮——这将弹出文件打开对话框,您可以从中选择您的 AUT。对于 Windows 程序,AUT 是应用程序的可执行文件(例如,addressbook.exe
),尽管也可以使用批处理文件(.bat)。选择 AUT 后,点击 完成,Squish 将创建一个与测试套件同名的子文件夹,并在该文件夹内创建一个名为 suite.conf
的文件,其中包含测试套件的配置详细信息。Squish 还将 AUT 注册到 squishserver。然后向导将关闭,squishide
将类似于下面的截图。
我们现在可以开始创建测试了。继续阅读以了解如何在不使用 squishide
的情况下创建测试套件,或跳转到 录制测试和验证点。
通过命令行创建测试套件
要创建一个新测试套件,请使用命令行:
- 创建一个新的目录来存放测试套件,目录名称应该以
suite
开头。在这个例子中,我们为Python测试创建了一个名为C:\Addressbook\suite_py
的目录——我们还把Addressbook.exe
复制到了C:\Addressbook
目录中。(我们也为其他语言创建类似的子目录,但这里只是为了示例,因为通常我们只为所有测试使用一种语言。) - 使用squishserver将Aut(自动测试工具)注册。
注意: 每个Aut必须与squishserver注册,这样测试脚本就不需要包含Aut的路径,从而使得测试平台无关。注册的另一个好处是可以在没有
squishide
的情况下测试Aut——例如在执行回归测试时。通过在命令行上执行带有
--config
选项和addAUT
命令的 squishserver 来完成这项操作。例如,假设我们在Windows上的squish
目录中squishserver --config addAUT Addressbook C:\Addressbook
我们必须给
addAUT
命令提供Aut的可执行文件名称,以及单独的Aut的路径。在这种情况下,路径是指测试套件配置文件中添加的Aut的可执行文件的路径。有关应用程序路径的更多信息,请参阅 Aut和设置,有关squishserver的命令行选项的更多信息,请参阅 squishserver。 - 在套件子目录中创建一个名为
suite.conf
的纯文本文件(ASCII或UTF-8编码)。这是测试套件的配置文件,它至少必须标识Aut、用于测试的脚本语言以及Aut使用的封装器(即GUI工具包或库)。文件的格式是key
=
value
,每行一个 key-value 对。例如AUT = Addressbook LANGUAGE = Python WRAPPERS = Windows OBJECTMAPSTYLE = script
Aut是上一步注册的名称,但不需要
.exe
或.bat
后缀。LANGUAGE可以设置为你喜欢的任何一种——目前Squish能够支持JavaScript、Python、Perl、Ruby和Tcl。WRAPPERS应设置为Windows
。
记录测试和验证点
Squish使用为测试套件指定的脚本语言来记录测试。一旦记录了一个测试,我们就可以 运行 该测试,Squish将忠实地重复我们在记录测试时执行的所有操作,但不会包含人类容易出现但计算机不需要的暂停。编辑记录的测试或将记录的测试的部分内容复制到手动创建的测试中也是可能的,并且非常常见,我们将在教程的后面看到。
记录是添加到现有测试用例中的。以下方式可以创建一个 新脚本文本测试用例
- 选择 文件 > 新测试用例 打开 新Squish测试用例向导,输入测试用例的名称,然后选择 完成。
- 点击位于“测试套件”视图的 测试用例 标签右侧的 新脚本文本测试用例( )工具栏按钮。这创建了一个带有默认名称的新测试用例,您可以轻松更改其名称。
为新测试用例命名为 "tst_general"。
Squish将自动在测试套件文件夹内部创建一个名为该名字的子文件夹,以及一个测试文件,例如 test.py
。如果您选择JavaScript作为脚本语言,则该文件名为 test.js
,对于Perl、Ruby或Tcl也类似。
如果您收到一个“Hello World”脚本而不是示例 .feature
文件,请点击 运行测试套件( )左侧的箭头并选择 新脚本文本测试用例( )。
要使测试脚本文件(如,test.js
或test.py
)在编辑器视图中显示,请根据首选项 > 常规 > 打开模式设置,点击或双击测试用例。这将选择脚本作为活动项,并使得相应的记录()和运行测试()按钮可见。
复选框用于控制当点击运行测试套件()工具栏按钮时会运行哪些测试用例。我们还可以通过点击其运行测试()按钮来运行单个测试用例。如果测试用例当前未处于活动状态,按钮可能不可见,直到鼠标悬停在其上。
最初,脚本中的main()
会将在测试结果中打印你好,世界。要手动创建测试,如下文教程中所做的那样,我们必须创建一个main
函数,并且应该在顶部导入相同的导入。对于Squish来说,main
的名字是特殊的。测试可以包含尽可能多的函数和其他代码,只要脚本语言允许。但是,当测试执行时(即运行),Squish始终执行main
函数。你可以像在如何创建和使用共享数据和共享脚本中描述的那样在测试脚本之间共享常用代码。
Squish还有两个与其他功能名称也特别相关:cleanup
和init
。有关更多信息,请参阅测试人员创建的特殊函数。
一旦创建了新的测试用例,我们可以手动编写测试代码或在测试中记录。点击测试用例的记录()按钮会替换测试的代码为新记录。或者,您可以记录片段并将它们插入到现有的测试用例中,如如何编辑和调试测试脚本中所述。
从命令行创建测试
要从命令行创建一个新的测试用例
- 在测试套件目录内创建一个新的子目录。例如,在
C:\Addressbook\suite_py
目录内,创建tst_general
目录。 - 在测试用例目录内创建一个名为
test.py
的文件(如果是使用JavaScript脚本语言,那么是test.js
,以及其他语言的类似情况)。
记录我们的第一个测试
在我们开始记录之前,让我们简要回顾一下非常简单的测试场景
- 打开
MyAddresses.adr
地址文件。 - 导航到第二个地址并添加一个新的名字和地址。
- 导航到第四个地址(现在是第三个地址)并更改姓字段。
- 导航到第一个地址并删除它。
- 验证第一个地址现在是刚刚添加的新地址。
我们现在可以记录我们的第一个测试了。点击测试套件视图“测试用例”列表中显示的tst_general
测试用例旁边的记录()。这将导致Squish运行AUT,以便我们可以与之交互。一旦AUT运行,执行以下操作——不要担心它需要多长时间,因为Squish不会记录空闲时间。
- 点击文件 > 打开,当文件对话框出现时,点击文件名
MyAddresses.adr
,然后点击打开按钮。 - 点击第二行,然后点击编辑 > 添加,然后在添加对话框的第一行中输入
Jane
。现在点击(或通过Tab键移动到)第二行编辑并输入Doe
。继续同样操作,设置电子邮箱地址为[email protected]
和电话号码为555 123 4567
。不必担心输入错误——正常使用退格键删除并修正即可。最后,点击确定按钮。现在应该会看到一个新添加的第二个地址,包含您输入的详细信息。 - 点击第四行第二列(姓氏列),删除其文本,并用
Doe
替换。(您可以通过简单地重叠输入来完成此操作。)然后按Enter键确认您的编辑。 - 现在点击第一行,然后点击编辑 > 删除,然后在消息框中点击是按钮。第一行应该被删除,因此您的
Jane Doe
条目现在应该是第一个。 - 在Squish控制栏(从左数第二个按钮)中点击验证工具栏按钮,然后点击属性。
这将使
squishide
出现。在应用程序对象视图中,展开Address_Book__MyAddresses_adr_Window_0
对象,然后展开Table_0
对象。点击TableRow_6
对象,然后点击Jane_TableCell_0
对象以使其属性出现在属性视图中,并检查text
属性的复选框。现在点击Doe_TableCell_1
对象并检查其text
属性。最后,点击保存并插入验证按钮(在验证点创建器视图的底部)以将第一行的名字和姓氏验证插入到记录的测试脚本中。(参见下面的截图。)一旦验证点插入,squishide
的窗口将会再次隐藏,控制栏窗口和AUT将重新可见。 - 我们现在已经完成了测试,所以在AUT中点击文件 > 退出,然后在消息框中点击否,因为我们不想保存任何更改。
当我们退出AUT后,记录的测试将在squishide
中如截图所示出现。(注意,记录的确切代码将根据您的交互方式而有所不同。例如,您可以通过单击或使用键序列来调用菜单选项——使用哪种方式无关紧要,但由于它们不同,Squish将不同地记录它们。)
如果记录的测试没有出现,请点击(或根据您的平台和设置双击)tst_general
测试用例,使Squish在编辑器窗口中显示测试的test.py
文件,如图所示。
现在我们已经记录了测试,我们应该能够回放它,即运行它。这本身就很有用,因为如果回放失败,这可能意味着应用程序已被破坏。此外,我们在其中放置的两种验证将在回放时进行检查,如图所示。
在测试录制过程中插入验证点非常方便。这里我们一次插入了两个,但我们可以根据需要随时插入任意数量的验证点。然而,有时我们可能会忘记插入一个验证,或者后来可能想要插入一个新的验证。正如我们将在下一节中看到的那样,我们可以轻松地将额外的验证插入到已录制的测试脚本中,请参阅插入附加验证点。
在继续之前,我们将探讨如何从命令行录制测试。然后我们将看到如何运行测试,并查看一些Squish生成的用于录制测试的代码以及讨论其一些功能。
从命令行录制测试
在录制测试时,squishserver必须始终处于运行状态。这由 squishide
自动处理,但命令行用户必须根据squishserver中的说明手动启动squishserver。
要从命令行录制测试,请执行squishrunner程序,并指定要录制的测试套件和测试用例的名称。例如,假设我们位于包含测试套件目录的目录中
squishrunner --testsuite suite_py --record tst_general --useWaitFor
最好使用--useWaitFor
选项进行录制,该选项记录对Object waitForObject(objectOrName)函数的调用,这比使用snooze(seconds)函数更可靠,尽管出于历史原因它是默认选项。《code translate="no">squishide 自动使用Object waitForObject(objectOrName)函数。
当您连接了多个设备和/或模拟器时,需要使用--device some-device
指定目标。
从IDE运行测试
要在 squishide
中运行测试用例,当测试用例在测试套件视图中悬停或选中时,点击出现的 运行测试 ()。
要按顺序运行两个或更多测试用例,或只运行选定的测试用例,请点击 运行测试套件 ()。
从命令行运行测试
在运行测试时,squishserver必须始终处于运行状态,或者必须为squishrunner提供--local
选项。更多信息请参阅squishserver。
要从命令行回放已录制的测试,请执行squishrunner程序,并指定包含我们的录制脚本的测试套件和我们想要播放的测试用例。例如,假设我们位于包含测试套件目录的目录中
squishrunner --testsuite suite_py --testcase tst_general --local
检查生成的代码
如果您查看截图中的代码(或下面显示的代码片段),您会看到它由许多 Object waitForObject(objectOrName) 和 Object waitForObjectItem(objectOrName, itemOrIndex) 调用组成,这些调用作为诸如 mouseClick(objectOrName)、clickButton(objectOrName) 和 type(objectOrName, text) 等其他调用的参数。该 Object waitForObject(objectOrName) 函数等待一个 GUI 对象准备好进行交互(即变为可见并可操作),然后跟随一些与该对象交互的函数。典型的交互是点击菜单、点击菜单选项或按钮,或输入一些文本。
有关 Squish 脚本命令的完整概述,请参阅 如何创建测试脚本、如何测试应用程序 - 特定内容、API 参考手册 和 工具参考手册。
对象由 Squish 生成的名称标识。有关详细说明,请参阅 如何标识和访问对象。
生成的代码少于 35 行。以下是代码的提取,仅显示 Squish 记录编辑菜单的“添加”选项、将 Jane Doe 的详细信息输入到“添加”对话框中,并在最后点击“确定”来关闭对话框并更新表格。
注意:尽管截图只显示了 Python 测试套件的操作,但对于这里和整个教程中引用的代码片段,我们显示 Squish 支持的所有脚本语言的代码。在实际操作中,您当然通常只使用其中之一,因此您可以只查看您感兴趣的代码片段,并跳过其他语言。
mouseClick(waitForObjectItem(names.address_Book_MyAddresses_adr_Menubar, "Edit")) mouseClick(waitForObjectItem(names.edit_MenuItem_2, "Add...")) type(waitForObject(names.address_Book_Add_Forename_Edit), "Jane") type(waitForObject(names.address_Book_Add_Forename_Edit), "<Tab>") type(waitForObject(names.address_Book_Add_Surname_Edit), "Doe") type(waitForObject(names.address_Book_Add_Surname_Edit), "<Tab>") type(waitForObject(names.address_Book_Add_Email_Edit), "[email protected]") type(waitForObject(names.address_Book_Add_Email_Edit), "<Tab>") type(waitForObject(names.address_Book_Add_Phone_Edit), "555 123 4567") clickButton(waitForObject(names.address_Book_Add_OK_Button))
mouseClick(waitForObjectItem(names.addressBookMyAddressesAdrMenubar, "Edit")); mouseClick(waitForObjectItem(names.editMenuItem_2, "Add...")); type(waitForObject(names.addressBookAddForenameEdit), "Jane"); type(waitForObject(names.addressBookAddForenameEdit), "<Tab>"); type(waitForObject(names.addressBookAddSurnameEdit), "Doe"); type(waitForObject(names.addressBookAddSurnameEdit), "<Tab>"); type(waitForObject(names.addressBookAddEmailEdit),"[email protected]"); type(waitForObject(names.addressBookAddEmailEdit),"<Tab>"); type(waitForObject(names.addressBookAddPhoneEdit), "555 123 4567"); clickButton(waitForObject(names.addressBookAddOKButton));
mouseClick(waitForObjectItem($Names::address_book_myaddresses_adr_menubar, "Edit")); mouseClick(waitForObjectItem($Names::edit_menuitem_2, "Add...")); type(waitForObject($Names::address_book_add_forename_edit), "Jane"); type(waitForObject($Names::address_book_add_forename_edit), "<Tab>"); type(waitForObject($Names::address_book_add_surname_edit), "Doe"); type(waitForObject($Names::address_book_add_surname_edit), "<Tab>"); type(waitForObject($Names::address_book_add_email_edit),"jane.doe\@nowhere.com"); type(waitForObject($Names::address_book_add_email_edit),"<Tab>"); type(waitForObject($Names::address_book_add_phone_edit), "555 123 4567"); clickButton(waitForObject($Names::address_book_add_ok_button));
mouseClick(waitForObjectItem(Names::Address_Book_MyAddresses_adr_Menubar, "Edit")) mouseClick(waitForObjectItem(Names::Edit_MenuItem_2, "Add...")) type(waitForObject(Names::Address_Book_Add_Forename_Edit), "Jane") type(waitForObject(Names::Address_Book_Add_Forename_Edit), "<Tab>") type(waitForObject(Names::Address_Book_Add_Surname_Edit), "Doe") type(waitForObject(Names::Address_Book_Add_Surname_Edit), "<Tab>") type(waitForObject(Names::Address_Book_Add_Email_Edit),"[email protected]") type(waitForObject(Names::Address_Book_Add_Email_Edit), "<Tab>") type(waitForObject(Names::Address_Book_Add_Phone_Edit), "555 123 4567") clickButton(waitForObject(Names::Address_Book_Add_OK_Button))
invoke mouseClick [waitForObjectItem $names::Address_Book_MyAddresses_adr_Menubar "Edit"] invoke mouseClick [waitForObjectItem $names::Edit_MenuItem_2 "Add..."] invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] "Jane" invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] "Doe" invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_Email_Edit] "[email protected]" invoke type [waitForObject $names::Address_Book_Add_Email_Edit] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_Phone_Edit] "555 123 4567" invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button]
如您所见,测试人员使用键盘在文本字段之间进行标签切换,并使用鼠标而不是键击点击确定。如果测试人员以任何其他方式点击按钮(例如,按 Alt+O,或切换到确定按钮并按空格键),结果将是相同的,但当然 Squish 会记录实际的动作。
Squish 组件使用前缀为 names.
的变量来引用对象,这些变量标识它们为符号名称。每个变量都包含相应的实际名称作为值,它可以是基于字符串的,或者作为属性到值的键值映射实现。Squish 支持几种命名方案,所有这些命名方案都可以在脚本中使用—混合。使用符号名称的优势是,如果应用程序发生变化,导致需要不同的名称,我们可以简单地更新 Squish 的对象映射(将符号名称与实际名称相关联),从而避免更改测试脚本。有关对象映射的更多信息,请参阅对象映射和对象映射视图。
当符号名称位于光标下时,编辑器的上下文菜单允许您打开符号名称,显示其在对象映射中的条目,或者转换为实际名称,这在脚本语言中放置一个内联映射在光标,允许您直接在脚本中编辑属性。
现在我们已经看到了如何录制和回放测试,以及Squish生成的代码,接下来我们进一步,确保在测试执行过程中特定点的条件成立。
插入额外的验证点
在前一节中,我们看到了在录制测试脚本时插入验证点的简便性。验证点也可以插入到现有的测试脚本中,通过设置断点和使用squishide
,或者简单地通过编辑测试脚本并添加对Squish测试函数的调用,如Boolean test.compare(value1, value2)和Boolean test.verify(condition)。
Squish支持许多种类的验证点:那些验证对象属性具有特定值的——称为“对象属性验证”;那些验证整个表格的内容符合我们预期的——称为“表格验证”;那些验证两张图片是否匹配的——称为“截图验证”;以及一种混合验证类型,它包括多个对象的属性和截图,称为“视觉验证”。此外,还可以验证搜索图是否存在于屏幕上,或者OCR能否找到某些文本。最常用的类型是对象属性验证,我们将在教程中介绍这部分内容。请参阅如何创建和使用验证点。
常规(非脚本化)属性验证点以XML文件的形式存储在测试用例或测试套件资源中,包含需要传递给test.compare()
的值。这些验证点可以在多个测试用例中重复使用,并在脚本代码的单行中验证多个值。
脚本化属性验证点是直接调用Boolean test.compare(value1, value2)函数,带有两个参数——特定对象的特定属性的值和预期值。我们可以在录制或手动编写的脚本中手动插入对Boolean test.compare(value1, value2)函数的调用,或者使用脚本化验证点让Squish为我们插入它们。在前一节中,我们展示了如何使用squishide
在录制时插入验证。在这里,我们将首先展示如何使用squishide
将验证插入到现有的测试脚本中,然后我们将展示如何手动插入一个验证。
在请求Squish插入验证点之前,最好确保我们有一个验证清单和验证时间点。我们可以向tst_general
测试用例添加许多潜在的验证,但既然我们的关注点只是展示如何做到这一点,我们只会做两个——我们将验证“Jane Doe”条目的电子邮件地址和电话号码是否与输入的一致,并将验证点直接放在我们录制时插入的点之后。
要使用squishide
插入验证点,我们首先在脚本中设置一个断点(无论是录制的还是手动编写的——对Squish来说都无关紧要),在我们想要验证的点。
如图所示,我们在第35行设置了断点。这是通过简单地双击,或者在编辑器旁的空白区域(行号旁边)右击并选择“添加断点”右键菜单项来实现的。我们选择该行,因为它紧随移除第一个地址的脚本行之后,因此在此点(在调用“文件”菜单关闭应用程序之前),第一个地址应该是“Jane Doe”。截图显示了在录制过程中使用“squishide
”输入的验证。我们的附加验证将跟随它们。(请注意,如果您以不同的方式录制测试,例如使用键盘快捷键而不是点击菜单项,则您的行号可能不同。)
设置断点后,我们像往常一样通过单击“运行测试”()按钮,或者通过单击“运行 > 运行测试用例”菜单选项来运行测试。与正常的运行测试不同,测试将在达到断点时停止(即第35行,或者您设置的任何行),Squish的主窗口将重新出现(这可能会遮挡AUT)。此时,“squishide
”将自动切换到“测试调试视角。”
视角和视图
“squishide
”的功能与Eclipse IDE相似。如果您不熟悉Eclipse,理解以下关键概念至关重要:即“视图”和“视角”。在Eclipse中,因此在“squishide
”中,一个“视图”基本上是一个子窗口,如停靠窗口或现有窗口中的选项卡。一个“视角”是一组一起排列的视图。两者都可以通过“窗口”菜单访问。
“squishide
”包含以下视角
您可以将这些视角更改以显示额外视图或隐藏不需要的视图,或创建包含您所需视图的视角。因此,如果窗口发生了很大变化,这只是意味着视角已更改。使用“窗口”菜单返回您想要的视角。然而,Squish会自动更改视角以反映当前情况,因此您通常不需要手动更改视角。
插入验证点
如图所示,当Squish在断点处停止时,“squishide
”将自动切换到“测试调试视角。”该视角显示了“变量视图”、“编辑器视图”、“调试视图”、“应用程序对象视图”和“属性视图”、“方法视图”和“测试结果视图”。
要插入一个验证点,我们可以展开“应用程序对象”视图中的项目,直到找到我们想要验证的对象。在这个例子中,我们想要验证DataGridView
的第一个行的文本,因此我们展开Address_Book__MyAddresses_adr_Window_0
项目,然后展开其子项目,直到我们找到Table_0
,然后在其中越过TableColumn
对象看到TableRow
对象,因为我们感兴趣的项目在一行中。我们展开第一个TableRow
以显示其TableCell
。第一个表格单元格(Jane_TableCell_0
)有我们想要验证的第一个文本。一旦我们点击项目对象(即表格单元格),其属性如截图所示将在<或缺>属性视图中显示。
任何时候都可以通过从< ArrayAdapter rampant="no">窗口菜单中选择来返回正常的< a href="main-window.html#ide-squish-test-management-perspective" translate="no">测试管理视图 (或通过单击其工具栏按钮),尽管如果在停止脚本或运行到完成时,< ArrayAdapter genutzt="no">squishide 将自动返回。
在这里,我们可以看到< code translate="no">TableRow_6项目中的< code translate="no">Jane_TableCell_0 code>的文本值是“Jane”;我们在录制过程中已经插入了一个验证。向下滚动,直到你可以看到< code translate="no">jane_doe_nowhere_com_TableCell_2项目:这是电子邮件地址。为了确保每次运行测试时都进行验证,请单击< a href="application-objects-view.html#ide-the-application-objects-view" translate="no">应用程序对象视图中的< code translate="no">jane_doe_nowhere_com_TableCell_2项目,以使其属性出现,然后检查文本。当我们检查它时,如截图所示,将出现验证点创建器视图。
此时,验证点尚未添加到测试脚本中。我们可以很容易地通过单击保存并插入验证按钮来添加它。但在做之前,我们还会添加另一件要验证的事项。
向下滚动并单击应用程序对象视图中的< code translate="no">555_123_4567_TableCell_3项;然后单击其< name="no">文本"属性。现在,这两个验证将如截图所示出现在验证点创建器视图中。
现在我们已经说明我们期望这些属性具有所显示的值,即电子邮件地址“[email protected]”和电话号码“555 123 4567”。我们必须单击< b translate="no">插入 按钮才能实际插入验证点,所以现在就做吧。
现在我们不需要继续运行测试,所以我们可以在此处停止运行测试(通过单击停止工具栏按钮),或者我们可以继续(通过单击恢复按钮)。
完成插入验证以及停止或运行测试后,我们现在应该禁用断点。只需右键单击断点,并在上下文菜单中单击禁用断点菜单选项。我们现在准备在没有断点但有验证点的情况下运行测试。单击运行测试(< esp//*[.nextDouble(*(src="images(run.png"))])>)按钮。这一次,我们将得到一些额外的测试结果——如截图显示的几个之一我们已展开以显示其详细信息。(我们还将选择Squish插入以进行验证的代码行——请注意,代码结构与录制过程中插入的代码结构相同。)
这些特定的验证点生成了四个测试,比较新插入条目的名字、姓氏、电子邮件和电话号码。
插入验证点的另一种方法是使用代码形式。在理论上,我们只需在现有脚本中的任何地方添加自己的Squish测试函数调用,例如 Boolean test.compare(value1, value2) 和 Boolean test.verify(condition)。在实践中,最好先确保Squish知道我们想要验证的对象,以便在测试运行时找到它们。这涉及到与使用squishide
插入相似的非常相似的程序。首先,我们在打算添加验证的地方设置一个断点。然后运行测试脚本,直到它停止。接下来,我们导航到应用程序对象视图,直到找到我们想要验证的对象。在这种情况下,明智的做法是右击我们感兴趣的对象,并点击将对象添加到对象图中的上下文菜单选项。这将确保Squish可以访问该对象。然后再次右键单击并点击复制符号名的上下文菜单选项——这将为我们提供Squish用于识别对象的名称。现在我们就可以编辑测试脚本,加入我们的验证并完成或停止执行。(不要忘了取消已不再需要的断点。)
尽管我们可以编写与自动生成的代码完全相同的风格的测试脚本代码,但通常以稍微不同的风格行事会更清晰、更容易,我们将在下一刻解释。
对于我们的手动验证,我们希望在测试结束前,即AUT终止之前,检查DataGridView
中出现的地址数量。截图显示了我们输入的用于获取此验证的代码行以及运行测试脚本的执行结果。
在手动编写脚本时,我们使用Squish的test
模块的函数来验证测试脚本执行过程中的某些条件。如图表(和下面的代码片段)所示,我们首先获取我们感兴趣的对象的引用。使用Object waitForObject(objectOrName)函数是手动编写的测试脚本中的标准做法。此函数等待对象可用(即可见且启用),然后返回对该对象的引用。(否则它将超时并引发可捕获异常。)然后我们使用此引用来访问项目属性——在这种情况下是DataGridView
的rowCount,并使用Boolean test.verify(condition)函数验证值是否是我们预期的。(顺便提一下,我们从上一行获取对象的名称,所以我们不需要设置断点并将表格的名称手动添加到对象图中,这样Squish就会记住它,因为在这种情况下Squish已经在测试录制期间添加了它。)
这是我们为所有支持的脚本语言输入的最后验证代码。当然,您只需查看您将用于自己的测试的代码即可。(对于大多数语言的绝大多数验证,我们使用Boolean test.verify(condition)函数的调用——但对于Tcl来说,往往更方便使用我们在此处所用的Boolean test.compare(value1, value2)函数。)
table = waitForObject(names.address_Book_MyAddresses_adr_Table) test.compare(table.rowCount, 125)
var table = waitForObject(names.addressBookMyAddressesAdrTable); test.compare(table.rowCount, 125);
my $table = waitForObject($Names::address_book_myaddresses_adr_table); test::compare($table->rowCount, 125);
table = waitForObject(Names::Address_Book_MyAddresses_adr_Table) Test.compare(table.rowCount, 125)
set table [waitForObject $names::Address_Book_MyAddresses_adr_Table_2] test compare [property get $table rowCount] 125
编码模式非常简单:我们获取我们感兴趣的对象的引用,然后使用Squish的验证函数之一验证其属性。当然,如果我们希望与对象交互,我们当然可以调用对象上的方法。
有关手动编写的测试示例,请参考手工创建测试、如何创建测试脚本和如何测试应用程序 - 专门说明。
有关验证点的全面覆盖,请参考如何创建和使用验证点。
测试结果
每次测试运行完成后,在底层squishide的Test Results视图中显示测试结果,包括验证点的结果。
这是测试运行的详细报告,也会包含任何失败或错误等的详细信息。如果您点击测试结果项,squishide将突出显示生成测试结果的脚本行。如果展开测试结果项,您可以查看测试的更多详细信息。
Squish的测试结果界面非常灵活。通过实现自定义报告生成器,可以以许多不同的方式处理测试结果,例如将它们存储在数据库中,或将它们输出为HTML文件。默认报告生成器仅在命令行运行Squish时将结果输出到stdout
,或在使用squishide时输出到测试结果视图。您可以通过单击测试结果并选择导出结果菜单选项从squishide保存测试结果为XML。有关报告生成器的列表,请参阅squishrunner –reportgen: 生成报告。还可以直接将测试结果记录到数据库中。请参阅如何从Squish测试脚本访问数据库。
如果您使用squishrunner在命令行上运行测试,还可以以不同的格式导出结果并将其保存到文件中。请参阅处理测试结果和如何使用测试语句部分以获得更多信息。
手工创建测试
我们已了解到如何记录测试并由插入验证点修改测试,现在我们可以看到如何手动创建测试。最简单的方法是通过修改和重构记录的测试,尽管从头开始创建手动测试也是完全可能的。
编写手动测试最具挑战性的部分可能是使用正确的对象名称,但在实践中,这很少成为问题。我们可以复制Squish在记录上次测试时已添加到对象映射的符号名或真实名称,或直接从记录的测试中复制对象名称。如果我们还没有记录任何测试并且从头开始,我们可以使用Spy。我们通过单击启动 AUT工具栏按钮来完成此操作。这启动AUT并切换到Spy视角。然后我们可以与AUT进行交互,直到我们感兴趣的对象可见。然后,在squishide中,我们使用对象选择器()或导航到应用程序对象树视图中的对象并使用上下文菜单将其添加到对象映射(以便Squish记住它)和到剪贴板(以便我们可以将其粘贴到我们的测试脚本中)。最后,我们可以单击退出 AUT工具栏按钮以终止AUT并将Squish返回到测试管理视角。请参阅如何使用Spy以获取有关使用Spy的更多详细信息。
我们可以通过点击 对象图 工具栏按钮()来查看对象图(也可参见,对象图视图)。Squish与之交互的每个应用程序对象都列在这里,无论是顶级对象还是子对象(视图为一个树状视图)。我们可以通过右键单击我们感兴趣的对象,然后单击上下文菜单的 复制符号名称(或者如果你想在脚本中修改它,可以选择 复制实际名称)来检索Squish在记录脚本中使用的符号名称。
修改和重构已记录的测试
假设我们想通过添加三个新的姓名和地址来测试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\\win\\Addressbook\\Addressbook.exe"') mouseClick(waitForObjectItem(names.address_Book_Unnamed_Menubar, "File")) mouseClick(waitForObjectItem(names.file_MenuItem, "Open..."))
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"'); mouseClick(waitForObjectItem(names.addressBookUnnamedMenubar, "File")); mouseClick(waitForObjectItem(names.fileMenuItem, "Open..."));
require 'names.pl'; use File::Spec; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\""); mouseClick(waitForObjectItem($Names::address_book_unnamed_menubar, "File")); mouseClick(waitForObjectItem($Names::file_menuitem, "Open..."));
require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"") mouseClick(waitForObjectItem(Names::Address_Book_Unnamed_Menubar, "File")) mouseClick(waitForObjectItem(Names::File_MenuItem, "Open..."))
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\"" invoke mouseClick [waitForObjectItem $names::Address_Book_Unnamed_Menubar "File"] invoke mouseClick [waitForObjectItem $names::File_MenuItem "Open..."]
代码的模式很简单:启动AUT,然后等待菜单栏,然后点击菜单栏;等待菜单项,然后点击菜单项。在这两种情况下,我们都使用了 Object waitForObjectItem(objectOrName, itemOrIndex) 函数。此函数用于多值对象(如列表、表格、树——或者在这种情况下,菜单栏和菜单),并且允许我们通过传递包含项目的对象名称和项目文本作为参数来访问对象的项目(当然,项目本身也是对象)。
注意:可能似乎把我们的函数放在 tst_adding
中是浪费,因为我们也可以在 tst_general
和其他测试用例中使用它们。然而,为了保持教程的简单,我们将代码放在 tst_adding
测试用例中。有关如何共享脚本的信息,请参见 如何创建和使用共享数据和共享脚本。
如果你查看记录的测试(tst_general
)或在对象映射中,你会看到Squish有时会为相同对象使用不同的名称。例如,菜单栏以两种不同的方式识别,最初为names.AddressBook_MenuBar
,然后如果用户点击文件 > 新建,它将识别为names.AddressBookUnnamed_MenuBar
,如果用户点击文件 > 打开并打开MyAddresses.adr
文件,则菜单栏将识别为namesAddressBookMyAddresses.adr_MenuBar
。之所以会发生这种情况,是因为Squish需要唯一地识别给定上下文中的每个对象,并使用启发式方法来确定哪些属性有助于识别。在识别主窗口的情况下,Squish使用窗口标题文本为其提供一些上下文。(例如,根据文件是否加载以及应用程序的状态,应用程序中的文件或编辑菜单可能会有不同的选项。)
当然,当我们编写测试脚本时,我们不想知道或关心使用哪种特定的名称变体,Squish通过提供备选命名方案来支持这一需求,我们很快就会看到。
如果在测试执行过程中AUT看起来冻结了,请等待Squish超时AUT(大约20秒),并显示对象未找到对话框,表明这种错误
这通常意味着Squish在对象映射中没有具有特定名称或属性值的对象。从这里,我们可以选择一个新对象,调试,抛出错误,或者在新对象选择后,重试。
选择新对象将更新符号名称的对象映射条目。除了对象选择器()外,我们还可以使用侦探的应用程序对象视图定位我们感兴趣的对象,并使用将对象添加到对象映射上下文菜单操作来访问它们的实际或符号名称。
命名很重要,因为这可能是编写脚本中最容易导致错误信息的部分,通常是上面显示的对象...未找到类型。一旦我们确定了测试中要访问的对象,使用Squish编写测试脚本就非常直接。特别是,Squish可能支持你最熟悉的脚本语言。
我们现在几乎准备好编写自己的测试脚本。最简单的方法是先录制一个模拟测试。因此,单击新建脚本测试用例()并将测试用例的名称设置为tst_dummy
。然后单击模拟测试用例的记录()。一旦AUT启动,单击文件 > 新建,然后单击(空)表格,然后单击编辑 > 添加并添加项目,然后按Return键或单击确定。最后,单击文件 > 退出完成,并单击“不保存更改”。然后仅为了确认一切正常,重新播放此测试。这样做的唯一目的是确保Squish将必要的名称添加到对象映射,因为这样做可能比使用Spy为每个感兴趣的对象都要快。重新播放模拟测试后,你可以根据需要删除它。
在对象映射中得到所有需要的对象名称后,我们现在可以从头开始编写自己的测试脚本。我们将从main()
开始,然后我们将查看main()
所使用的方法。
import names import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '\\examples\\win\\Addressbook\\Addressbook.exe"') table = waitForObject({"type": "Table"}) invokeMenuItem("File", "New") 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) test.compare(waitForObject(table).rowCount, len(data)) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"'); var table = waitForObject({"type": "Table"}); invokeMenuItem("File", "New"); test.verify(table.rowCount == 0); var data = new Array(new Array("Andy", "Beach", "[email protected]", "555 123 6786"), new Array("Candy", "Deane", "[email protected]", "555 234 8765"), new Array("Ed", "Fernleaf", "[email protected]", "555 876 4654")); for (var row = 0; row < data.length; ++row) addNameAndAddress(data[row]); test.compare(waitForObject(table).rowCount, data.length); closeWithoutSaving() }
require 'names.pl'; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\""); my $table = waitForObject({"type" => "Table"}); invokeMenuItem("File", "New"); 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(waitForObject($table)->rowCount, scalar(@data)); closeWithoutSaving(); }
require 'squish' require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"") table = waitForObject({:type => "Table"}) invokeMenuItem("File", "New") 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(waitForObject(table).rowCount, data.length) closeWithoutSaving end
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\"" set table [waitForObject [::Squish::ObjectName type Table]] 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] } test compare [property get [waitForObject $table] rowCount] [llength $data] closeWithoutSaving }
我们从调用 ApplicationContext startApplication(autName) 函数开始应用程序。我们传递的字符串名称是已在 Squish 中注册的名称(通常是可执行文件的名称)。然后我们获取 DataGridView
的引用。我们使用的对象名称在 tst_general
测试用例记录时没有被放入对象映射中,所以我们记录了一个虚拟测试来确保名称被添加——当然我们同样可以使用间谍工具。然后我们将名称从对象映射复制到我们的代码中。Object waitForObject(objectOrName) 函数等待一个对象准备好(可见且已启用)并返回其引用——或者超时并抛出一个可捕获的异常。我们使用一个 符号 名称来访问表格——这些是 Squish 记录测试时使用的名称——而不是真实/多属性名称(我们很快将看到示例)。一旦我们有了 table
引用,我们就可以用它来访问 DataGridView
的任何公共方法和属性。
invokeMenuItem()
是我们为这个测试专门编写的函数。它接受一个菜单名称和一个菜单选项名称,并调用该菜单选项。在利用 invokeMenuItem()
函数执行 文件 > 新建 之后,我们验证表格的行数为 0。
接下来,我们创建一些样本数据,并调用自定义的 addNameAndAddress()
函数,使用 AUT 的添加对话框来填充表格中的数据。然后我们再次比较表格的行数,这一次是要与我们的样本数据中的行数进行比较。最后,我们调用自定义的 closeWithoutSaving()
函数来终止应用程序。
现在我们将审查每个辅助函数,以便涵盖 tst_adding
测试用例中的所有代码,我们从 invokeMenuItem()
开始。
def invokeMenuItem(menu, item): mouseClick(waitForObjectItem({"type": "Menubar"}, menu)) mouseClick(waitForObject({'type': 'MenuItem', 'text' : item}))
function invokeMenuItem(menu, item) { mouseClick(waitForObjectItem({'type':'Menubar'}, menu)); mouseClick(waitForObject({'type':'MenuItem', 'text': item})); }
sub invokeMenuItem { my ($menu, $item) = @_; mouseClick(waitForObjectItem({"type" => "Menubar"}, $menu)); mouseClick(waitForObject({'type' => 'MenuItem', 'text' => $item})); }
def invokeMenuItem(menu, item) mouseClick(waitForObjectItem({:type => "Menubar"}, menu)) mouseClick(waitForObject({:type => 'MenuItem', :text => item})) end
proc invokeMenuItem {menu item} { invoke mouseClick [waitForObjectItem [::Squish::ObjectName type Menubar] $menu] invoke mouseClick [waitForObject [::Squish::ObjectName type MenuItem text $item]] }
正如我们之前提到的,Squish 用于菜单和菜单项(及其他对象)的符号名称可能会根据上下文而变化,并且通常以窗口标题的开头为起点。对于将当前文件名放入标题中的应用程序——例如地址簿示例——名称将包括文件名,我们必须考虑到这一点。
在地址簿示例中,主窗口的标题是“地址簿”(在启动时),或者是“地址簿 - 未命名”(在 文件 > 新建 之后,但在 文件 > 保存 或 文件 > 另存为 之前),或者是“地址簿 - filename”,其中 filename 当然可能不同。我们的代码通过使用真实(多属性)名称来处理所有这些情况。
符号名称是带有 names.
前缀的变量。真实名称通常是多属性名称,它指定了匹配属性的键值映射,以识别所需的对象。多属性名称必须指定至少类型。在这里,我们使用类型来唯一标识 MenuBar
(因为应用程序只有一个),并使用类型和文本来唯一标识 MenuItem
。
一旦我们确定了要交互的对象,我们就使用Object waitForObject(objectOrName)函数或Object waitForObjectItem(objectOrName, itemOrIndex)函数获取该对象的引用,然后使用mouseClick(objectOrName)函数点击它。该Object waitForObject(objectOrName)和Object waitForObjectItem(objectOrName, itemOrIndex)函数会使Squish暂停,直到指定的对象(在后者的情况下还包括项目)可见且启用。因此,在这里,我们等待菜单栏及其菜单栏项目中的一个,然后再等待菜单项。每次等待结束后,我们都会使用mouseClick(objectOrName)函数点击对象(或其项目)。
def addNameAndAddress(oneNameAndAddress): invokeMenuItem("Edit", "Add...") type(waitForObject(names.address_Book_Add_Forename_Edit), oneNameAndAddress[0]) type(waitForObject(names.address_Book_Add_Surname_Edit), oneNameAndAddress[1]) type(waitForObject(names.address_Book_Add_Email_Edit), oneNameAndAddress[2]) type(waitForObject(names.address_Book_Add_Phone_Edit), oneNameAndAddress[3]) clickButton(waitForObject(names.address_Book_Add_OK_Button))
function addNameAndAddress(oneNameAndAddress) { invokeMenuItem("Edit", "Add..."); type(waitForObject(names.addressBookAddForenameEdit), oneNameAndAddress[0]); type(waitForObject(names.addressBookAddSurnameEdit), oneNameAndAddress[1]); type(waitForObject(names.addressBookAddEmailEdit), oneNameAndAddress[2]); type(waitForObject(names.addressBookAddPhoneEdit), oneNameAndAddress[3]); clickButton(waitForObject(names.addressBookAddOKButton)); }
sub addNameAndAddress { my (@oneNameAndAddress) = @_; invokeMenuItem("Edit", "Add..."); type(waitForObject($Names::address_book_add_forename_edit), $oneNameAndAddress[0]); type(waitForObject($Names::address_book_add_surname_edit), $oneNameAndAddress[1]); type(waitForObject($Names::address_book_add_email_edit), $oneNameAndAddress[2]); type(waitForObject($Names::address_book_add_phone_edit), $oneNameAndAddress[3]); clickButton(waitForObject($Names::address_book_add_ok_button)); }
def addNameAndAddress(oneNameAndAddress) invokeMenuItem("Edit", "Add...") type(waitForObject(Names::Address_Book_Add_Forename_Edit), oneNameAndAddress[0]) type(waitForObject(Names::Address_Book_Add_Surname_Edit), oneNameAndAddress[1]) type(waitForObject(Names::Address_Book_Add_Email_Edit), oneNameAndAddress[2]) type(waitForObject(Names::Address_Book_Add_Phone_Edit), oneNameAndAddress[3]) clickButton(waitForObject(Names::Address_Book_Add_OK_Button)) end
proc addNameAndAddress {oneNameAndAddress} { invokeMenuItem "Edit" "Add..." invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] [lindex $oneNameAndAddress 0] invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] [lindex $oneNameAndAddress 1] invoke type [waitForObject $names::Address_Book_Add_Email_Edit] [lindex $oneNameAndAddress 2] invoke type [waitForObject $names::Address_Book_Add_Phone_Edit] [lindex $oneNameAndAddress 3] invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button] }
对于每一组名称和地址数据,我们通过调用Edit > Add菜单选项来弹出Add对话框。然后对于每个接收到的值,我们通过等待相应的Edit控件准备好,然后使用type(objectOrName, text)函数输入文本来填充适当的字段。然后我们键入一个选项卡以使焦点进入下一个可聚焦的小部件,仍然使用type(objectOrName, text)函数。最后按回车键点击对话框的OK。我们通过从记录的
def closeWithoutSaving(): invokeMenuItem("File", "Quit") clickButton(waitForObject(names.address_Book_No_Button))
function closeWithoutSaving() { invokeMenuItem("File", "Quit"); clickButton(waitForObject(names.addressBookNoButton)); }
sub closeWithoutSaving { invokeMenuItem("File", "Quit"); clickButton(waitForObject($Names::address_book_no_button)); }
def closeWithoutSaving invokeMenuItem("File", "Quit") clickButton(waitForObject(Names::Address_Book_No_Button)) end
proc closeWithoutSaving {} { invokeMenuItem "File" "Quit" invoke clickButton [waitForObject $names::Address_Book_No_Button] }
在这里,我们使用invokeMenuItem()
来执行File > Quit,然后点击Save unsaved changes?对话框的No。最后一行是从记录的测试中复制的。
整个测试大约有30行代码——如果将一些常见函数(如invokeMenuItem()
和closeWithoutSaving()
)放入共享脚本中,则会更少。大量代码是直接从记录的测试中复制的,并在某些情况下进行了参数化。
这应该足以给出编写AUT测试脚本的感观。请记住,Squish提供了比这里使用的多得多的功能(所有这些都在API参考和工具参考中有详细的说明)。此外,Squish还提供了访问AUT对象全部公共API的权限。
然而,测试用例的一个方面不是很令人满意。尽管像我们这里那样嵌入测试数据对于少量数据来说是合理的,但它相当有限,特别是当我们想要使用大量测试数据时。此外,我们没有测试添加到数据的任何部分,以查看它是否正确地出现在DataGridView
中。在下文中,我们将创建这个测试的新版本,但这一次我们将从外部数据源中获取数据,并检查添加到DataGridView
中的数据是否正确。
创建数据驱动测试
在前一节中,我们在测试中固定了三个名称和地址。但是,如果我们想测试大量的数据呢?或者如果我们想在不必更改测试脚本源代码的情况下更改数据,该怎么办?一种方法是将数据集导入Squish中,并使用数据集作为我们插入测试的值的来源。Squish可以导入以下格式的数据:.tsv
(制表符分隔值格式)、.csv
(逗号分隔值格式)、.xls
或.xlsx
(Microsoft Excel电子表格格式)。
注意:假设.csv
和.tsv
文件使用Unicode UTF-8编码——与所有测试脚本使用的编码相同。
测试数据可以使用squishide
或手动使用文件管理器或控制台命令导入。我们将描述这两种方法,从使用squishide
开始。
对于地址簿应用程序,我们想要导入MyAddresses.tsv
数据文件。我们首先单击文件 > 导入测试资源以弹出导入Squish资源对话框。在对话框中,单击浏览按钮选择要导入的文件——在本例中为MyAddresses.tsv
。确保将导入为组合框设置为“测试数据”。默认情况下,squishide
将只导入当前测试案例的测试数据,但我们需要测试数据对所有测试套件的测试案例都可用:为此,请选中复制到测试套件以共享单选按钮。现在单击完成按钮。现在您可以在测试套件资源视图(在测试数据选项卡中)中看到文件列表,如果您单击文件名,它将在编辑视图中显示。截图显示添加测试数据后的Squish。
要从squishide
外部导入测试数据,请使用文件管理器,例如文件资源管理器或查找器,或控制台命令。在测试套件目录中创建一个名为shared
的目录。然后,在shared
目录中创建名为testdata
的目录。将数据文件(在本例中为MyAddresses.tsv
)复制到shared\testdata
目录。
如果squishide
正在运行,请重新启动它。如果您单击测试套件资源视图的测试数据选项卡,您应该看到数据文件。单击文件名以在编辑视图中查看文件。
尽管在现实生活中我们会修改我们的tst_adding
测试案例以使用测试数据,但对于本教程的目的,我们将创建一个名为tst_adding_data
的新测试案例,它是tst_adding
的副本,我们将对其进行修改以使用测试数据。
我们唯一要更改的函数是main
,在该函数中,我们不是遍历硬编码的数据项,而是遍历数据集中的所有记录。我们还需要更新末尾的预期行数,因为我们现在要添加更多的记录,我们还将添加一个验证所添加每个记录的函数。
import names import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '\\examples\\win\\Addressbook\\Addressbook.exe"') table = waitForObject({"type": "Table"}) invokeMenuItem("File", "New") 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") addNameAndAddress((forename, surname, email, phone)) insertionRow = row # Modern Python: insertionRow = row if row >= 1 else 1 if row < 1: insertionRow = 1 checkNameAndAddress(insertionRow, record) if row > limit: break test.compare(table.rowCount, row + 1) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"'); var table = waitForObject({"type": "Table"}); invokeMenuItem("File", "New"); test.verify(table.rowCount == 0); var limit = 10; // To avoid testing 100s of rows since that would be boring var records = testData.dataset("MyAddresses.tsv"); for (var row = 0; row < records.length; ++row) { var record = records[row]; var forename = testData.field(record, "Forename"); var surname = testData.field(record, "Surname"); var email = testData.field(record, "Email"); var phone = testData.field(record, "Phone"); addNameAndAddress(new Array(forename, surname, email, phone)); var insertionRow = row < 1 ? 1 : row; checkNameAndAddress(insertionRow, record); if (row > limit) break; } test.compare(table.rowCount, row + 1); closeWithoutSaving() }
require 'names.pl'; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\""); my $table = waitForObject({"type" => "Table"}); invokeMenuItem("File", "New"); 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"); addNameAndAddress($forename, $surname, $email, $phone); my $insertionRow = $row < 1 ? 1 : $row; checkNameAndAddress($insertionRow, $record); if ($row > $limit) { last; } } test::compare($table->rowCount, $row + 1); closeWithoutSaving(); }
require 'squish' require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"") table = waitForObject({:type => "Table"}) invokeMenuItem("File", "New") 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") addNameAndAddress([forename, surname, email, phone]) insertionRow = row >= 1 ? row : 1 checkNameAndAddress(insertionRow, record) break if row > limit rows += 1 end Test.compare(table.rowCount, rows + 1) closeWithoutSaving end
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\"" set table [waitForObject [::Squish::ObjectName type Table]] test compare [property get $table rowCount] 0 invokeMenuItem "File" "New" 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] addNameAndAddress $details set insertionRow $row if {$row < 1} { set insertionRow 1 } checkNameAndAddress $insertionRow $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)函数来检索每个记录的各个字段。
使用测试数据填充了DataGridView
后,我们希望确认表格中的数据与我们添加的数据相同,这就是为什么我们添加了checkNameAndAddress()
。我们还对比较的记录数量设置了限制,以便测试更快地运行。由于Addressbook应用是在当前行之前插入行——除非表格为空,在这种情况下在第一行插入,我们必须特别计算插入行。此外,Windows DataGridView
使用基于1的索引,而Squish的数据处理函数使用基于0的索引,因此我们还必须考虑这种差异。
def checkNameAndAddress(insertionRow, record): for column in range(1, 1 + len(testData.fieldNames(record))): cell = waitForObject({'container' : names.table, 'row': insertionRow, 'column' : column, 'type' :'TableCell'}) test.compare(cell.text, testData.field(record, column - 1))
function checkNameAndAddress(insertionRow, record) { for (var column = 1; column <= testData.fieldNames(record).length; ++column) { var cell = waitForObject({'container' : names.table, 'row': insertionRow, 'column' : column, 'type' : 'TableCell'}); test.compare(cell.text, testData.field(record, column - 1)); } }
sub checkNameAndAddress { my($insertionRow, $record) = @_; my @columnNames = testData::fieldNames($record); for (my $column = 1; $column <= scalar(@columnNames); $column++) { my $cell = waitForObject({'container' => $Names::table, 'row' => $insertionRow, 'column'=> $column, 'type' => 'TableCell'}); test::compare($cell->text, testData::field($record, $column - 1)); } }
def checkNameAndAddress(row, record) for column in 1..TestData.fieldNames(record).length cell = waitForObject({:container => Names::Table, :row => row, :column => column, :type => 'TableCell'}) Test.compare(cell.text, TestData.field(record, column - 1)) end end
proc checkNameAndAddress {insertionRow record} { set columns [llength [testData fieldNames $record]] for {set column 1} {$column <= $columns} {incr column} { set cell [waitForObject [::Squish::ObjectName container $names::Table type TableCell row $insertionRow column $column]] test compare [property get $cell text] \ [testData field $record [expr {$column - 1}]] } }
此函数比较给定插入行(即,刚刚添加的数据)中DataGridView
的数据与相应记录(即,用于添加的数据)。我们使用Squish的SequenceOfStrings testData.fieldNames(record)函数获取列数,然后使用Boolean test.compare(value1, value2)函数检查表中的每个值与我们用于测试的数据中的值相同。如前所述,Squish的数据处理函数使用基于0的索引,但Windows的DataGridView
使用基于1的索引,因此我们的测试代码必须考虑这些差异。在这里,插入行和列是1基于的,所以我们使用String testData.field(record, fieldName)函数时从中减去1。
截图显示在运行数据驱动的测试之后Squish的测试摘要日志。
Squish还可以执行关键字驱动测试。这比数据驱动测试更复杂。请参阅如何进行关键字驱动测试。
了解更多
我们现在已经完成了教程。Squish可以执行比我们在这里展示的更多操作,但目标是尽快、尽可能轻松地让您开始基本测试。有关如何创建测试脚本如何创建测试脚本和有关如何测试应用程序的详细信息如何测试应用程序 - 特定细节部分提供了许多更多示例,包括展示测试如何与特定输入元素(如复选框、单选按钮、文本框和文本区域)交互的示例。
API参考和工具参考提供了Squish测试API的完整细节以及它提供的多个函数,以使测试尽可能容易和高效。阅读如何创建测试脚本和如何测试应用程序 - 特定细节以及浏览API参考和工具参考是值得的。您投资的时间将得到回报,因为您将知道Squish提供什么功能,可以避免重新发明已经存在的东西。
教程:设计行为驱动开发(BDD)测试
本教程将向您展示如何创建、运行和修改示例应用程序的行为驱动开发(BDD)测试。您将了解Squish最常见的功能。在本教程结束时,您将能够为您的应用程序编写自己的测试。
在本章中,我们将使用一个简单的地址簿应用作为我们的应用程序。这是一个基本的应用程序,允许用户加载现有的地址簿或创建一个新的,添加、编辑和删除条目。截图显示了用户添加新姓名和地址的应用程序运行情况。
BDD(行为驱动开发)简介
行为驱动开发(BDD)是测试驱动开发(TDD)的扩展,它将验收标准的定义置于开发过程的开端,而不是在软件开发完成后编写测试。在此之后可能还有代码变更的循环。
行为驱动测试是由一组功能
文件构建而成,通过一个或多个场景
描述产品的特性,这些场景
表现了期望的应用行为。每个场景
由一系列步骤组成,这些步骤代表了为该场景
需要测试的动作或验证。
BDD侧重于预期的应用行为,而不是实现细节。因此,BDD测试使用人类可读的领域特定语言(DSL)来描述。由于这种语言非技术性,此类测试不仅可以由程序员创建,还可以由产品所有者、测试人员或业务分析师创建。另外,在产品开发过程中,这些测试充当着活的产品文档。对于Squish的使用,应该使用Gherkin语法创建BDD测试。之前编写的(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
上文中大部分为自由文本(不一定必须是英语)。只有功能
/场景
结构和像给予
、并且
、当
和然后
这样的固定关键字是固定的。这些关键字中的每一个都标记了一个定义前提条件、用户动作和预期结果的步骤。应用程序行为的描述可以传递给软件开发者来实现这些功能,同时也可以将相同的描述传递给软件测试人员来实现自动化测试。
测试实现
创建测试套件
首先,我们需要创建一个测试套件,它是一个所有测试用例的容器。启动squishide,选择文件 > 新建测试套件。请遵循新建测试套件向导,提供一个测试套件名称,选择你喜欢的Windows Toolkit和脚本语言,并最终注册AddressBook.exe应用程序作为应用程序。请参阅创建测试套件的更多关于创建新测试套件的详细信息。
创建测试用例
Squish提供了两种测试用例类型:“脚本测试用例”和“BDD测试用例”。由于“脚本测试用例”是默认的,为了创建新的“BDD测试用例”,我们需要使用上下文菜单,通过点击新建脚本测试用例旁边的展开按钮( )并选择新建BDD测试用例。squishide
将记住你的选择,并且将在今后点击按钮时将BDD测试用例设置为默认。
新创建的BDD测试用例包含一个test.feature
文件(在创建新的BDD测试用例时填充了Gherkin模板),一个名为test.(py|js|pl|rb|tcl)
的文件,该文件将驱动执行(无需编辑此文件),以及一个名为steps/steps.(py|js|pl|rb|tcl)
的测试用例资源文件,其中将放置步实现代码。
我们需要为地址簿示例应用程序替换Gherkin模板中的Feature
。为此,复制下面的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
,请点击Record( )按钮旁边的相应Scenario
,它在测试用例资源的Scenarios选项卡中列出。
这将使Squish运行AUT,我们可以与之交互。此外,将显示控制栏,其中列出所有需要记录的步骤。现在所有与AUT的交互或添加到脚本的任何验证点都将记录在当前步骤Given addressbook application is running
(在控制栏上对应步骤的左侧加粗显示)下。为了验证先决条件已满足,我们将添加一个验证点。为此,在控制栏中点击Verify并选择Properties。
结果是,squishide
将放入Spy视角,显示了所有应用程序对象及其属性。在应用程序对象视图中选择Address_Book
对象。选择它将更新其右侧的属性视图。接下来,在属性视图中点击前面的复选框。最后,点击保存并插入验证。squishide
消失,控制栏再次显示。
完成每个步骤后,我们可以通过点击控制栏中位于当前步骤左侧的Finish Recording Step( )箭头按钮,移动到下一个未定义的步骤(回放之前定义的步骤)。
接下来,对于步骤When I create a new addressbook
,点击地址簿工具栏中的New按钮( ),然后点击Finish Recording Step( )。
最后,对于步骤Then addressbook should have zero entries
,验证包含地址条目的表格为空。在记录此验证时,点击记录时的Verify,然后选择Properties。在应用程序对象视图中,导航或使用对象选择器( )工具选择(不勾选)包含地址簿条目的TableView
对象(在我们的情况下,此表为空)。从属性视图检查rowCount属性,然后点击保存并插入验证。最后,点击控制栏中最后一个Finish Recording Step( )箭头按钮。
因此,Squish会在位于测试用例 > 测试用例资源(steps.*文件)中生成以下步骤定义。
@Given("addressbook application is running") def step(context): startApplication('"' + os.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"') test.compare(waitForObjectExists(names.address_Book_Unnamed_Window).enabled, True) @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.new_ToolbarItem)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, 0)
Given("addressbook application is running", function(context) { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"'); test.compare(waitForObjectExists(names.addressBookUnnamedWindow).enabled, true); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.newToolbarItem)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, 0); });
Given("addressbook application is running", sub { my $context = shift; startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\""); test::compare(waitForObjectExists($Names::address_book_unnamed_window)->enabled, 1); }); When("I create a new addressbook", sub { my $context = shift; clickButton(waitForObject($Names::new_toolbaritem)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, 0); });
Given("addressbook application is running") do |context| startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"") Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Window).enabled, true) end When("I create a new addressbook") do |context| clickButton(waitForObject(Names::New_ToolbarItem)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, 0) end
Given "addressbook application is running" {context} { startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\"" test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Window] enabled] true } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::New_ToolbarItem] invoke mouseClick [waitForObject $names::New_ToolbarItem] } Then "addressbook should have zero entries" {context} { test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] 0 }
由于记录了startApplication()
调用,应用程序将在第一步开始时自动启动。在每个场景结束时,都会调用OnScenarioEnd
钩子,导致在应用程序上下文中调用detach()
。由于AUT是用startApplication()
启动的,这会导致它终止。该钩子函数位于脚本视图中测试用例资源的bdd_hooks.(py|js|pl|rb|tcl)
文件中。您可以在其中定义额外的钩子函数。关于所有可用钩子的列表,请参阅通过钩子在测试执行期间执行操作。
@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有不同类型的参数,如any
、integer
或word
,允许我们的步骤定义更加可重用。让我们向我们的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 book
和 Then '1' entries should be present
。其余步骤已经有匹配的步骤实现。
要记录缺失的步骤,请点击测试用例资源中Scenario
旁边的记录()。脚本将播放,直到达到缺失的步骤,然后提示您实现它。如果您选择添加按钮,则可以输入新条目的信息。点击完成记录步骤()以转到下一个步骤。对于第二个缺失的步骤,我们可以记录一个像Then addressbook should have zero entries
步骤一样的对象属性验证。或者我们可以在steps.(py|js|pl|rb|tcl)
文件中复制该步骤的实现,并在test.compare
行的末尾增加数字。我们测试的不是一个项目,而是零个项目。
现在我们通过用参数类型替换值来参数化生成的步骤实现。由于我们想能够添加不同的名字,将'John'替换为'|word|'。请注意,每个参数将按照在步骤描述性名称中出现的顺序传递给步骤实现函数。通过将键入的值编辑成关键词来完成参数化,如下例步骤所示When I add a new person 'John', 'Doe','[email protected]','500600700' to address book
@When("I add a new person '|word|','|word|','|any|','|integer|' to address book") def step(context, forename, surname, email, phone): clickButton(waitForObject(names.add_ToolbarItem)) type(waitForObject(names.address_Book_Add_Forename_Edit), forename) type(waitForObject(names.address_Book_Add_Surname_Edit), surname) type(waitForObject(names.address_Book_Add_Email_Edit), email) type(waitForObject(names.address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(names.address_Book_Add_OK_Button))
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) { mouseClick(waitForObject(names.addToolbarItem)); type(waitForObject(names.addressBookAddForenameEdit), forename); type(waitForObject(names.addressBookAddSurnameEdit), surname); type(waitForObject(names.addressBookAddEmailEdit), email); type(waitForObject(names.addressBookAddPhoneEdit), phone); clickButton(waitForObject(names.addressBookAddOKButton)); });
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::add_toolbaritem)); type(waitForObject($Names::address_book_add_forename_edit), $forename); type(waitForObject($Names::address_book_add_surname_edit), $surname); type(waitForObject($Names::address_book_add_email_edit), $email); type(waitForObject($Names::address_book_add_phone_edit), $phone); clickButton(waitForObject($Names::address_book_add_ok_button)); });
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::Add_ToolbarItem)) type(waitForObject(Names::Address_Book_Add_Forename_Edit), forename) type(waitForObject(Names::Address_Book_Add_Surname_Edit), surname) type(waitForObject(Names::Address_Book_Add_Email_Edit), email) type(waitForObject(Names::Address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(Names::Address_Book_Add_OK_Button)) end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke mouseClick [waitForObject $names::Add_ToolbarItem] invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] $forename invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] $surname invoke type [waitForObject $names::Address_Book_Add_Email_Edit] $email invoke type [waitForObject $names::Address_Book_Add_Phone_Edit] $phone invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button] }
如果我们记录了最终的Then
作为缺失的步骤,并验证表中的行数为1,则可以修改该步骤以使其接受参数,这样以后可以验证其他整数值。
@Then("'|integer|' entries should be present") def step(context, entryCount): test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, entryCount)
Then("'|integer|' entries should be present", function(context, entryCount) { test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, entryCount); });
Then("'|integer|' entries should be present", sub { my $context = shift; my ($entryCount) = @_; test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, $entryCount); });
Then("'|integer|' entries should be present") do |context, entryCount| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, entryCount) end
Then "'|integer|' entries should be present" {context entryCount} { test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] $entryCount }
在表中为步骤提供参数
下一个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 | forname | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present
处理此类表格的步骤实现如下
@When("I add new persons to address book") def step(context): table = context.table # Drop initial row with column headers table.pop(0) for (forename, surname, email, phone) in table: clickButton(waitForObject(names.add_ToolbarItem)) type(waitForObject(names.address_Book_Add_Forename_Edit), forename) type(waitForObject(names.address_Book_Add_Surname_Edit), surname) type(waitForObject(names.address_Book_Add_Email_Edit), email) type(waitForObject(names.address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(names.address_Book_Add_OK_Button))
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]; mouseClick(waitForObject(names.addToolbarItem)); type(waitForObject(names.addressBookAddForenameEdit), forename); type(waitForObject(names.addressBookAddSurnameEdit), surname); type(waitForObject(names.addressBookAddEmailEdit), email); type(waitForObject(names.addressBookAddPhoneEdit), phone); clickButton(waitForObject(names.addressBookAddOKButton)); } });
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::add_toolbaritem)); type(waitForObject($Names::address_book_add_forename_edit), $forename); type(waitForObject($Names::address_book_add_surname_edit), $surname); type(waitForObject($Names::address_book_add_email_edit), $email); type(waitForObject($Names::address_book_add_phone_edit), $phone); clickButton(waitForObject($Names::address_book_add_ok_button)); } });
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 mouseClick(waitForObject(Names::Add_ToolbarItem)) type(waitForObject(Names::Address_Book_Add_Forename_Edit), forename) type(waitForObject(Names::Address_Book_Add_Surname_Edit), surname) type(waitForObject(Names::Address_Book_Add_Email_Edit), email) type(waitForObject(Names::Address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(Names::Address_Book_Add_OK_Button)) 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::Add_ToolbarItem] invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] $forename invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] $surname invoke type [waitForObject $names::Address_Book_Add_Email_Edit] $email invoke type [waitForObject $names::Address_Book_Add_Phone_Edit] $phone invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button] } }
步骤和场景之间共享数据
让我们向 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.add_ToolbarItem)) type(waitForObject(names.address_Book_Add_Forename_Edit), forename) type(waitForObject(names.address_Book_Add_Surname_Edit), surname) type(waitForObject(names.address_Book_Add_Email_Edit), email) type(waitForObject(names.address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(names.address_Book_Add_OK_Button)) 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) { mouseClick(waitForObject(names.addToolbarItem)); type(waitForObject(names.addressBookAddForenameEdit), forename); type(waitForObject(names.addressBookAddSurnameEdit), surname); type(waitForObject(names.addressBookAddEmailEdit), email); type(waitForObject(names.addressBookAddPhoneEdit), phone); clickButton(waitForObject(names.addressBookAddOKButton)); 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::add_toolbaritem)); type(waitForObject($Names::address_book_add_forename_edit), $forename); type(waitForObject($Names::address_book_add_surname_edit), $surname); type(waitForObject($Names::address_book_add_email_edit), $email); type(waitForObject($Names::address_book_add_phone_edit), $phone); clickButton(waitForObject($Names::address_book_add_ok_button)); $context->{'userData'} = {}; $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| mouseClick(waitForObject(Names::Add_ToolbarItem)) type(waitForObject(Names::Address_Book_Add_Forename_Edit), forename) type(waitForObject(Names::Address_Book_Add_Surname_Edit), surname) type(waitForObject(Names::Address_Book_Add_Email_Edit), email) type(waitForObject(Names::Address_Book_Add_Phone_Edit), phone) clickButton(waitForObject(Names::Address_Book_Add_OK_Button)) 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 mouseClick [waitForObject $names::Add_ToolbarItem] invoke type [waitForObject $names::Address_Book_Add_Forename_Edit] $forename invoke type [waitForObject $names::Address_Book_Add_Surname_Edit] $surname invoke type [waitForObject $names::Address_Book_Add_Email_Edit] $email invoke type [waitForObject $names::Address_Book_Add_Phone_Edit] $phone invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button] $context userData [dict create forename $forename surname $surname] }
所有存储在 context.userData 中的数据都可以在任何 Feature
的所有 Scenarios
的所有步骤和 Hooks
中访问。最后,我们需要实现步骤 Then previously entered forename and surname shall be at the top
。
@Then("previously entered forename and surname shall be at the top") def step(context): test.compare(waitForObjectExists(names.o1_1_TableCell).text, context.userData['forename']) test.compare(waitForObjectExists(names.o1_2_TableCell).text, context.userData['surname'])
Then("previously entered forename and surname shall be at the top", function(context) { test.compare(waitForObjectExists(names.o11TableCell).text, context.userData["forename"]); test.compare(waitForObjectExists(names.o12TableCell).text, context.userData["surname"]); });
Then("previously entered forename and surname shall be at the top", sub { my $context = shift; test::compare( waitForObject($Names::o1_1_tablecell)->text, $context->{'userData'}{'forename'}); test::compare( waitForObject($Names::o1_2_tablecell)->text, $context->{'userData'}{'surname'}); });
Then("previously entered forename and surname shall be at the top") do |context| Test.compare(waitForObjectExists(Names::O1_1_TableCell).text, context.userData[:forename]) Test.compare(waitForObjectExists(Names::O1_2_TableCell).text, context.userData[:surname]) end
Then "previously entered forename and surname shall be at the top" {context} { test compare [property get [waitForObjectExists $names::1_1_TableCell] text] [dict get [$context userData] forename] test compare [property get [waitForObjectExists $names::1_2_TableCell] text] [dict get [$context userData] surname] }
场景概述
假设我们的 Feature
包含以下两个 Scenarios
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
如我们所见,那些 Scenarios
使用不同的测试数据执行相同的行为。这可以通过使用 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>','<surname>','<email>','<phone>' to address book Then '1' entries should be present Examples: | forename | surname | email | phone | | John | Doe | john@m.com | 500600700 | | Bob | Koo | bob@m.com | 500600800 |
请注意,OnScenarioEnd
钩子将执行在每个 Scenario Outline
的循环迭代的末尾。
测试执行
在 squishide
中,用户可以执行 Feature
中的所有 Scenarios
,或者只执行一个所选的 Scenario
。为了执行所有 Scenarios
,必须通过在测试套视图中的 Play 按钮上单击来执行适当的测试用例。
为了只执行一个 Scenario
,您需要打开 Feature
文件,在指定的 Scenario
上右键单击并选择 Run Scenario。另一种方法是单击 Run Test()按钮,该按钮位于 Test Case Resources 的 Scenarios 选项卡中的相应 Scenario
旁边。
执行 Scenario
后,Feature
文件将根据执行结果进行着色。更详细的信息(例如日志)可以在测试结果视图中找到。
测试调试
Squish 提供了在任何点暂停测试用例执行以检查脚本变量、监视应用程序对象或在 Squish Script Console 中运行自定义代码的功能。为此,必须在启动执行前在 Feature
文件中的任何包含步骤的行或任何执行代码的行(即在步骤定义代码的中间)放置一个断点。
断点到达后,您可以检查所有应用程序对象及其属性。如果断点放置在步骤定义或钩子中,那么您可以额外添加验证点或记录代码片段。
重用步骤定义
通过在其他目录中使用的测试用例中重用步骤定义,可以增加 BDD 测试的可维护性。有关更多信息,请参阅 collectStepDefinitions()。
教程:将现有测试迁移到 BDD
本章面向已有Squish测试的用户,希望引入行为驱动测试。第一部分描述了如何保留现有测试并使用BDD方法添加新测试。第二部分描述了如何将现有套件转换为BDD。
将现有测试扩展到BDD
第一个选项是保留现有Squish测试,并通过添加新的BDD测试来扩展它们。可以有一个包含基于脚本测试案例和BDD功能文件的《测试套件》。只需打开现有的《测试套件》,然后在下拉列表中选择“新建BDD测试案例”选项。
假设您的现有脚本测试使用了库,并且您正在调用共享函数以与AUT交互,这些函数也可以在BDD步骤实现中使用。下面的示例中,一个函数被多个基于脚本的测试案例使用
def createNewAddressBook():
clickButton(waitForObject(names.new_ToolbarItem))
function createNewAddressBook(){
clickButton(waitForObject(names.newToolbarItem));
}
sub createNewAddressBook{
clickButton(waitForObject($Names::new_toolbaritem));
}
def createNewAddressBook
clickButton(waitForObject(Names::New_ToolbarItem))
end
proc createNewAddressBook {} { invoke clickButton [waitForObject $names::New_ToolbarItem] }
新BDD测试案例可以轻松使用相同的函数
@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
}
将现有测试转换为BDD
第二个选项是将包含基于脚本测试案例的现有《测试套件》转换为行为驱动测试。由于《测试套件》可以包含基于脚本和BDD测试案例,迁移可以逐步进行。包含两种测试案例类型的《测试套件》可以被执行,结果进行分析,不需要额外的工作。
第一步是回顾现有《测试套件》中的所有测试案例,并根据它们所测试的《功能》分组。每个基于脚本的测试案例将转换为《场景》,它是《功能》的一部分。例如,假设我们有5个基于脚本的测试案例。经过审查后,我们发现这些基于脚本的测试案例检验了两个《功能》。因此,在迁移完成后,我们的测试套件将包含两个BDD测试案例,每个测试案例包含一个《功能》。每个《功能》将包含多个《场景》。在我们的示例中,第一个《功能》包含三个《场景》,第二个《功能》包含两个《场景》。
一开始,在包含计划迁移到BDD的基于脚本的Squish测试的`squishide`中打开一个《测试套件》。接下来,通过选择右边的“新建脚本测试案例”( )右侧的上下文菜单中的“新建BDD测试案例”选项来创建一个新的测试案例。每个BDD测试案例对应一个包含一个《功能》以及一个或多个《场景》的《test.feature》文件。接下来,打开《test.feature`文件,使用Gherkin语言描述《功能`。根据模板的语法,编辑《功能`名称,并提供简短描述(可选)。然后,决定在脚本测试案例中执行哪些动作和验证应该迁移到步骤。以下是地址簿应用程序的示例测试案例
def main(): startApplication("Addressbook") test.log("Create new addressbook") clickButton(waitForObject(names.new_ToolbarItem)) test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, 0)
function main() { startApplication("Addressbook"); test.log("Create new addressbook"); clickButton(waitForObject(names.newToolbarItem)); test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, 0); }
sub main { startApplication("Addressbook"); test::log("Create new addressbook"); clickButton(waitForObject($Names::new_toolbaritem)); test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, 0); }
def main startApplication("Addressbook") Test.log("Create new addressbook") clickButton(waitForObject(Names::New_ToolbarItem)) Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, 0) end
proc main {} { startApplication "Addressbook" test log "Create new addressbook" invoke clickButton [waitForObject $names::New_ToolbarItem] test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] 0 }
分析上述基于脚本的测试案例后,我们可以创建以下《场景`并将其添加到《test.feature`文件中
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('"' + os.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"') test.compare(waitForObjectExists(names.address_Book_Unnamed_Window).enabled, True) @When("I create a new addressbook") def step(context): mouseClick(waitForObject(names.new_ToolbarItem)) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.address_Book_Unnamed_Table).rowCount, 0)
Given("addressbook application is running", function(context) { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '\\examples\\win\\Addressbook\\Addressbook.exe"'); test.compare(waitForObjectExists(names.addressBookUnnamedWindow).enabled, true); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.newToolbarItem)); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.addressBookUnnamedTable).rowCount, 0); });
Given("addressbook application is running", sub { my $context = shift; startApplication("\"$ENV{'SQUISH_PREFIX'}\\examples\\win\\Addressbook\\Addressbook.exe\""); test::compare(waitForObjectExists($Names::address_book_unnamed_window)->enabled, 1); }); When("I create a new addressbook", sub { my $context = shift; clickButton(waitForObject($Names::new_toolbaritem)); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::address_book_unnamed_table)->rowCount, 0); });
Given("addressbook application is running") do |context| startApplication("\"#{ENV['SQUISH_PREFIX']}\\examples\\win\\Addressbook\\Addressbook.exe\"") Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Window).enabled, true) end When("I create a new addressbook") do |context| clickButton(waitForObject(Names::New_ToolbarItem)) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::Address_Book_Unnamed_Table).rowCount, 0) end
Given "addressbook application is running" {context} { startApplication "\"$::env(SQUISH_PREFIX)\\examples\\win\\Addressbook\\Addressbook.exe\"" test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Window] enabled] true } When "I create a new addressbook" {context} { invoke mouseClick [waitForObject $names::New_ToolbarItem] invoke mouseClick [waitForObject $names::New_ToolbarItem] } Then "addressbook should have zero entries" {context} { test compare [property get [waitForObjectExists $names::Address_Book_Unnamed_Table] rowCount] 0 }
注意,在将此脚本测试迁移到BDD时已经移除了 test.log("Create new addressbook")
。当执行步骤 我创建一个新的地址簿
时,步骤名称将被记录到测试结果中,所以 test.log
调用将是多余的。
此外,当测试用例执行结束时,Squish将终止AUT。在每次 Scenario
结束时,由自动生成的包含 OnScenarioEnd
挂钩的钩子文件也将终止AUT。
@OnScenarioEnd
def hook(context):
currentApplicationContext().detach()
OnScenarioEnd(function(context) {
currentApplicationContext().detach();
});
OnScenarioEnd(sub { currentApplicationContext()->detach(); });
OnScenarioEnd do |context| currentApplicationContext().detach() end
OnScenarioEnd { context } { applicationContext [currentApplicationContext] detach }
©2024 Qt公司有限公司。本文件的文档贡献是各自所有者的版权。
本提供的文档在Free Software Foundation发布的GNU自由文档许可协议版本1.3的条款下许可。
Qt及其相关标志是Qt公司有限公司在芬兰和/或世界其他国家的商标。所有其他商标均为其各自所有者的财产。