Squish for Web 教程
学习如何测试Web应用。
教程:开始测试Web应用
Squish内置了IDE和命令行工具。使用squishide
是最简单也是最有效的方式开始,但一旦你构建了大量测试,你可能希望自动化它们。例如,为了运行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或shell脚本中运行的命令行工具是很有价值的。
注意: 如果您需要视频指导,Qt学院提供了关于Squish基本使用的45分钟在线课程(
我们使用一个简单的地址簿应用作为我们的AUT。该应用程序与Squish一起在SQUISHDIR/examples/web/addressbook
中提供。这是一个非常基本的应用,允许用户与一个模拟的现有地址簿交互或创建一个新的,添加、编辑和删除条目。尽管应用程序很简单,但它具有大多数标准Web应用的所有关键功能:按钮、单选按钮、行编辑、弹出对话框以及一个中央区域——在这种情况下显示一个表格。您学习测试该应用程序的所有想法和实践都可以很容易地适应您自己的应用。有关测试Web特定功能和标准编辑小部件的更多示例,请参阅如何创建测试脚本和如何测试Web应用。
注意: 在整个手册中,我们经常提到SQUISHDIR
目录。这意味着Squish安装的目录,可能是C:\Squish
、/usr/local/squish
、/opt/local/squish
或其他,具体取决于您安装的位置。确切的位置无关紧要,只要您在看到本手册中的路径和文件名时,将SQUISHDIR
目录心理上翻译成实际目录即可。
截图显示了应用程序的操作。
使用示例
本教程的示例是一个包含在文件SQUISHDIR/examples/web/addressbook/AddressBook.html
中的HTML和JavaScript Web应用。Squish for Web旨在测试通过Web服务器在http:上提供的真实Web应用,并且AddressBook示例需要在至少一个Web服务器上托管。
服务器是用Python编写的;只需使用系统上的Python解释器,或在SQUISHDIR
下(python2
或python3
)运行它即可。
$ python SQUISHDIR/examples/web/addressbook/server.py
如果您使用的是Windows
C:\> cd SQUISHDIR\examples\web\addressbook C:\SQUISHDIR\examples\web\addressbook> ..\..\..\python2\python server.py
一旦服务器运行,您可以使用URL http://localhost:9090/AddressBook.html
访问Web地址簿示例应用程序。(如果端口9090与您的机器上其他任何东西冲突,只需将未使用的端口号作为命令行参数传递给server.py
,它会使用该端口号。当然,如果使用不同的端口号,在整个教程中必须使用该端口号。)
Windows安全对话框
当在Windows上第一次启动任何TCP/IP服务器(包括squishserver,或server.py
)时,根据您的安全设置,Windows可能会弹出对话框询问您是否要允许或阻止服务器运行。如果您收到此对话框,您必须选择解除阻止,以便Squish能够正常运行。
移动平台上的浏览器
为了测试运行在iOS(例如iPhone和iPad)或Android设备上的浏览器内的Web应用程序(如Safari或Chrome),一旦安装了Squish for Web,就需要进行一些特定设备的额外设置。请参阅移动设备上的浏览器。
Firefox用户
一个技术限制阻止了Firefox浏览器已在运行时对测试的录制或回放。如果您尝试执行,Firefox将显示一个新的标签页,但测试不会运行。解决方案是关闭Firefox。然后,当您录制或回放测试时,Squish会自动根据需要启动并关闭Firefox。
Squish概念
在接下来的章节中,我们将创建一个测试套件然后创建一些测试,但在那之前,我们将简要回顾一些关键的Squish概念。
要执行测试,您需要
- 一个要测试的应用程序,称为测试应用程序(AUT)。
- 一个测试脚本,用于对AUT进行测试。
Squish方法的一个基本方面是,AUT和对其进行的测试脚本始终在两个单独的过程中执行。这确保即使AUT崩溃,也应该不会影响Squish。在这种情况下,测试脚本将优雅地失败并记录错误消息。除了隔离Squish和测试脚本以防止AUT崩溃之外,在单独的过程中运行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 对象,如按钮、菜单、菜单项、标签和表格控件等时,主要使用术语 widget。Windows 用户可能对术语 control 和 container 更加熟悉,但在此我们使用“widget”一词来表示两者。同样,macOS 用户可能习惯于使用 view 这一术语。
为了让测试脚本能够查询和控制 AUT,Squish 必须能够访问 AUT 的内部结构,这可以通过使用 绑定 来实现。绑定实际上是库,它们提供了对对象以及对对象的属性和方法(进而可访问)的访问。Web 对象 API 的绑定 (Web Object API) 是一组可操作的属性和方法,主要用于操作加载网页中的 HTML 对象。
创建测试套件
测试套件是一组或多个测试用例(测试)。使用测试套件方便,因为它使得在一系列相关的测试之间共享脚本和测试数据变得简单。
在此,以及在教程的其余部分,我们首先将描述如何使用 squishide
来完成事情,对于命令行用户的相关信息将在后面提供。
在 squishide
中创建测试套件
启动 squishide
,可以通过单击或双击图标,通过从任务栏菜单启动 squishide 或通过在命令行上执行 squishide 来完成,您可以选择适合您所使用平台的方式。一旦 Squish 启动,您可能会看到一个 欢迎页面。单击右上角的 工作台 按钮来关闭它。然后,squishide 将看起来与屏幕截图相似,但可能因为使用的窗口系统、颜色、字体和主题而略有不同。
Squish 启动后,单击 文件 > 新建测试套件,将弹出下面的 新建 Squish 测试用例向导。
为您的测试套件输入一个名称,并选择您要存储测试套件的文件夹。在屏幕截图中,我们将测试套件命名为 suite_js
并将其放置在 addressbook
文件夹中。(对于您的测试,您可能使用一个更有意义的名称,如 suite_addressbook
;我们选择 suite_js
是因为教程中我们将创建多个套件,每个套件针对 Squish 支持的每种脚本语言一个。)当然,您可以选择您喜欢的任何名称和文件夹。完成详细信息后,单击 下一步 以进入 工具集(或 脚本语言)页面。
如果您得到了此向导页面,请单击您的 AUT 使用的工具集。在这个示例中,我们必须单击 Web,因为我们正在测试一个 Web 应用程序。然后单击 下一步 以进入 脚本语言 页面。
注意:虽然截图仅显示了Python测试套件运行情况,但在此处和整个教程中引用的代码片段,我们展示了Squish支持的所有脚本语言的代码。实际上,你通常只会使用其中之一,所以请随意查看你感兴趣的代码片段,并跳过其他语言。
选择你想要的脚本语言——唯一限制是每个测试套件只能使用一种脚本语言。(因此,如果你想使用多种脚本语言,只需创建多个测试套件,每个套件对应你想要使用的脚本语言。) Squish为所有语言提供相同的功能。
选择脚本语言后,点击完成以完成新测试套件的创建。此时,Squish将创建一个与测试套件同名的子文件夹,并在该文件夹内创建一个名为suite.conf
的文件,其中包含测试套件的配置细节。然后,向导将关闭,squishide
的外观将与下面的截图类似。
我们现在准备开始创建测试。继续阅读以了解如何在不使用squishide
的情况下创建测试套件,或者如果你想,可以跳转到录制测试和验证点。
我们现在准备录制第一个测试。
录制测试和验证点
Squish使用为测试套件指定的脚本语言来录制测试。一旦某个测试被记录,我们可以运行该测试,Squish将忠实地重复我们在录制测试时执行的所有操作,但不会像人类那样出现暂停,这是计算机不需要的。也可以编辑录制的测试,或者将录制的测试的部分内容复制到手动创建的测试中,就像在本教程后面的部分我们将看到的那样。
录制将添加到现有测试用例中。你可以以下方式创建一个新的脚本测试用例
- 选择文件 > 新测试用例以打开新Squish测试用例向导,输入测试用例名称,并选择完成。
- 点击位于测试套件视图中测试用例标签右侧的新脚本测试用例( )工具栏按钮。这会创建一个具有默认名称的新测试用例,你可以轻松更改其名称。
给新测试用例命名为"tst_general"。
Squish将自动在测试套件文件夹内创建一个同名的子文件夹,并创建一个测试文件,例如test.py
。如果你选择JavaScript作为脚本语言,文件名为test.js
,对于Perl、Ruby或Tcl也是如此。
如果你收到的是.feature
文件而不是“Hello World”脚本,点击运行测试套件( )左侧的箭头,并选择新脚本测试用例( )。
要让测试脚本文件(如,test.js
或test.py
)在编辑视图中出现,点击或双击测试用例,具体取决于首选项 > 常规 > 打开模式设置。这会选择脚本作为活动选项,并使对应的记录( )和运行测试( )按钮可见。
复选框用于控制当点击运行测试套件()工具栏按钮时运行哪些测试用例。我们还可以通过点击其运行测试()按钮运行单个测试用例。如果测试用例当前不可用,按钮可能不可见,直到鼠标悬停在其上。
最初,脚本的main()
会将Hello World记录到测试结果中。要手动创建测试,就像我们在教程的后半部分将要做的,必须创建一个main
函数,并且在顶部导入库应该相同。对于Squish来说,main
这个名字是特别的。测试可以包含任何你喜欢的函数和其他代码,这取决于脚本语言支持,但是在测试执行(即运行)时,Squish总是执行main
函数。你可以像在如何创建和使用共享数据和共享脚本中描述的那样在测试脚本之间共享常用代码。
另外两个函数名对于Squish来说也是特别的:cleanup
和init
。更多信息,请参阅测试者创建的特殊函数。
一旦创建新的测试用例,我们就可以手动编写测试代码或记录测试。点击测试用例的记录()按钮将用一个新的录制替换测试的代码。或者,你可以记录片段并将它们插入到现有的测试用例中,具体请参阅如何编辑和调试测试脚本。
录制我们的第一个测试
在我们开始录制之前,简要回顾一下我们这个非常简单的测试场景
- 添加一个新的姓名和地址。
- 更改第四个姓名和地址的姓字段。
- 删除第二个姓名和地址
- 验证最后一个地址是否为添加的新地址。
我们现在准备记录我们的第一个测试。点击测试套件视图中测试用例列表中显示的tst_general
测试用例右侧的记录()。对于Web测试,Squish会提示浏览器的启动URL。在这个教程中,应该是http://localhost:9090/AddressBook.html
一旦浏览器运行并且页面加载完毕,执行以下操作——不要担心它需要多长时间,因为Squish不会记录空闲时间
- 点击添加按钮,并在表中填写姓名为“Jane”,姓为“Doe”,电子邮件地址为“[email protected]”,电话号码为“555 123 4567”。点击或按Tab键在字段之间导航。最后,点击保存按钮。现在应该有一个新的最后一个地址包含您输入的详细信息。
- 点击第四行的复选框以选择该记录,然后点击编辑按钮。在表标签页或点击姓字段并将姓改为“Doe”。最后,点击保存按钮。更改应反映在地址列表中。
- 点击第二行的复选框以选择该记录,然后点击删除按钮。点击弹出确认对话框的确定按钮。更改应反映在地址列表中。
- 点击Squish 控制栏窗口(从左数第二个按钮)中的验证工具栏按钮并选择属性。
这将使
squishide
出现。在 应用对象 视图中,点击 对象选择器()按钮,然后在 AUT 中将鼠标移动到最后的名字(即之前输入的 "Jane")—每个鼠标悬停的网页元素都应被一个红色轮廓突出显示。一旦正确的表格单元格被突出显示(即,“Jane”),点击它。现在回到squishide
并检查 属性视图 的 innerText 属性。现在再次点击 对象选择器()按钮,这次点击 AUT 中显示的最后一个姓氏(即,“Doe”)。现在再次回到squishide
并再次检查 innerText 属性。最后,点击 保存并插入验证 按钮(在 验证点创建器视图 的底部),将所选行的名字和姓氏验证插入到录制测试脚本中。(见下面的截图。)一旦插入验证点,squishide
的窗口将再次隐藏,控制栏窗口 和 AUT 将再次显示。 - 现在已经完成了测试,所以点击 控制栏窗口 的 停止录制 按钮(最左边的按钮)。
录制完成之后,录制的测试将如截图所示出现在 squishide
中。具体记录的代码将根据你的交互方式而有所不同。例如,您可以通过点击或使用键盘序列来调用菜单选项——您使用哪种方法都无关紧要,但既然它们不同,Squish 会以不同的方式记录它们)
如果记录的测试没有出现,点击(或根据您的平台和设置双击)tst_general
测试用例;这将使 Squish 在一个编辑器窗口中显示测试的 test.js
文件,如下截图所示。
运行测试
现在我们已经录制了测试,我们可以回放它,即运行它。这本身就很实用,因为如果回放失败,可能意味着应用程序已被破坏。此外,我们将插入的两个验证将在回放时进行检查,如截图所示。
在测试录制过程中插入验证点是极其方便的。在这里,我们一次性插入了两个,但我们可以在测试录制过程中根据自己的需要多次插入一个或多个验证点。然而,有时我们可能忘记插入一个验证点,或者以后可能想要插入一个新的验证点。就像我们将在下一节 插 入额外的验证点 中看到的那样,我们很容易将额外的验证点插入到录制的测试脚本中。
回放失败
即使您正确地遵循了说明,测试用例仍然无法回放。这是因为在编辑步骤中,HTML 输入字段的选中状态没有正确记录/回放,所以在 typeText
回放时,输入的字符串被附加(或在某些浏览器上是前置)到之前的值上。建议的解决方案是在上述行中插入 HTML_TextBase.selectAll() 的调用,该调用直接在 HTML 元素本身上执行,从 Web 对象 API。以下是在所有脚本语言中的示例。
clickButton(waitForObject(names.editButton_button)) # manually inserted selectAll(): waitForObject(names.oneitem_surnameEdit_text).selectAll() typeText(waitForObject(names.oneitem_surnameEdit_text), "Doe") clickButton(waitForObject(names.save_button)) clickButton(waitForObject(names.froglogicAddressbookEditButtonButton)); // manually inserted selectAll(): waitForObject(names.froglogicAddressbookSurnameEditText).selectAll(); typeText(waitForObject(names.froglogicAddressbookSurnameEditText), "Doe"); clickButton(waitForObject(names.froglogicAddressbookSaveButton)); clickButton(waitForObject($Names::editbutton_button)); # manually inserted selectAll(): waitForObject($Names::oneitem_surnameedit_text)->selectAll(); typeText(waitForObject($Names::oneitem_surnameedit_text), "Doe"); clickButton(waitForObject($Names::save_button)); clickButton(waitForObject(Names::EditButton_button)) # manually inserted selectAll() waitForObject(Names::Oneitem_surnameEdit_text).selectAll() typeText(waitForObject(Names::Oneitem_surnameEdit_text), "Doe") clickButton(waitForObject(Names::Save_button)) invoke clickButton [waitForObject $names::editButton_button] # manually inserted selectAll: invoke [waitForObject $names::oneitem_surnameEdit_text] selectAll invoke typeText [waitForObject $names::oneitem_surnameEdit_text] "Doe" invoke clickButton [waitForObject $names::Save_button]
从IDE运行测试
在 squishide
中运行测试用例,在 测试套件视图 中将鼠标悬停或选择测试用例时,将会出现 运行测试 () 按钮。
运行两个或更多测试用例依次进行或只运行所选测试用例,请单击 运行测试套件 ()。
从命令行运行测试
在运行测试时,squishserver 必须始终运行,或者必须为 squishrunner 提供带有 --local
选项的配置。更多详细信息,请参阅 squishserver。
要从命令行播放已录制的测试,我们需要执行 squishrunner 程序并指定我们的录制脚本所在的测试套件以及我们想要播放的测试用例。例如,假设我们位于包含测试套件目录的目录中
squishrunner --testsuite suite_js --testcase tst_general --local
检查生成的代码
如果您查看截图中的代码(或者下面显示的代码片段),您将看到它由许多 Object waitForObject(objectOrName) 调用组成,这些调用是其他各种调用的参数,如 typeText(objectOrName, text) 和 clickButton(objectOrName)。函数 Object waitForObject(objectOrName) 等待一个 GUI 对象准备好交互(即,变为可见并启用),然后跟随着一些与该对象交互的函数。典型的交互操作包括激活(弹出)菜单、单击菜单选项或按钮,或者输入一些文本。
有关 Squish 脚本命令的完整概述,请参阅 如何创建测试脚本、如何测试应用程序 - 特定方面、API 参考手册 和 工具参考手册。
对象通过 Squish 生成的名称识别。有关详细信息,请参阅 如何识别和访问对象。
注意:虽然截图仅显示了Python测试套件运行情况,但在此处和整个教程中引用的代码片段,我们展示了Squish支持的所有脚本语言的代码。实际上,你通常只会使用其中之一,所以请随意查看你感兴趣的代码片段,并跳过其他语言。
生成的代码大约有 20 行。以下是一个提取,仅显示了 Squish 录制单击 编辑 菜单的 添加 选项、将 Jane Doe 的详细信息输入到 添加 对话框中以及在最后单击 确定 来关闭对话框并更新表的内容。
clickButton(waitForObject(names.addButton_button)) typeText(waitForObject(names.oneitem_forenameEdit_text), "Jane") typeText(waitForObject(names.oneitem_surnameEdit_text), "Doe") typeText(waitForObject(names.oneitem_emailEdit_text), "[email protected]") typeText(waitForObject(names.oneitem_phoneEdit_text), "555 123 4567") clickButton(waitForObject(names.save_button))
clickButton(waitForObject(names.addButtonButton)); typeText(waitForObject(names.froglogicAddressbookForenameEditText), "Jane"); typeText(waitForObject(names.froglogicAddressbookSurnameEditText), "Doe"); typeText(waitForObject(names.froglogicAddressbookEmailEditText), "[email protected]"); typeText(waitForObject(names.froglogicAddressbookPhoneEditText), "123 555 4567"); clickButton(waitForObject(names.froglogicAddressbookSaveButton));
clickButton(waitForObject($Names::addbutton_button)); typeText(waitForObject($Names::oneitem_forenameedit_text), "Jane"); typeText(waitForObject($Names::oneitem_surnameedit_text), "Doe"); typeText(waitForObject($Names::oneitem_emailedit_text), "jane.doe\@nowhere.com"); typeText(waitForObject($Names::oneitem_phoneedit_text), "555 123 4567"); clickButton(waitForObject($Names::save_button));
clickButton(waitForObject(Names::AddButton_button)) typeText(waitForObject(Names::Oneitem_forenameEdit_text), "Jane") typeText(waitForObject(Names::Oneitem_surnameEdit_text), "Doe") typeText(waitForObject(Names::Oneitem_emailEdit_text), "[email protected]") typeText(waitForObject(Names::Oneitem_phoneEdit_text), "555 123 4567") clickButton(waitForObject(Names::Save_button))
invoke clickButton [waitForObject $names::addButton_button] invoke typeText [waitForObject $names::oneitem_forenameEdit_text] "Jane" invoke typeText [waitForObject $names::oneitem_surnameEdit_text] "Doe" invoke typeText [waitForObject $names::oneitem_emailEdit_text] "[email protected]" invoke typeText [waitForObject $names::oneitem_phoneEdit_text] "555 123 4567" invoke clickButton [waitForObject $names::Save_button]
当 AUT 显示地址列表时,将显示 添加 和 编辑 按钮。当单击 添加 或 编辑 按钮时,列表将隐藏,并显示一个表单,可以在其中添加新地址或编辑所选地址。表单包含 保存 和 取消 按钮——单击其中任何一个按钮,表单都将隐藏,并且地址列表将再次显示。
缩放录音指的是使用以“names.”前缀开头的变量来引用对象,这用于标识为符号名。每个变量作为其值包含相应的真实名称,可以是基于字符串的实现,也可以作为属性到值的键值映射。Squish支持多种命名方案,所有这些命名方案都可以在脚本中使用和混合。使用符号名的优点是,如果应用程序发生变化导致需要不同的名称,我们只需更新Squish的对象映射(将符号名关联到真实名称),就可以避免更改测试脚本。有关对象映射的更多信息,请参阅对象映射和对象映射视图。
当符号名位于光标下时,编辑器的上下文菜单允许您打开符号名,显示其在对象映射中的条目,或转换到真实名称,这在您的脚本语言中将映射放置在光标处,允许您在脚本本身中手动编辑属性。
如果您查看记录的测试(tst_general
)或对象映射,您会看到Squish有时会使用层次名称,这些名称描述了HTML页面测试的DOM(文档对象模型)中的路径。有关Squish对Web中的命名和查找Web对象的更多信息,请参阅Squish for Web使用的命名生成算法和如何查找和查询Web对象。
现在,我们已经了解了如何录制和回放测试以及Squish生成的代码,让我们更进一步,确保测试执行中特定点的条件成立。
插入额外的验证点
在前一节中,我们看到在记录测试脚本期间插入验证点是多么容易。验证点也可以通过设置断点和使用squishide
或直接编辑测试脚本并在其中放置对Squish测试函数的调用(如布尔test.compare(value1, value2)和布尔test.verify(condition))插入到现有的测试脚本中。
Squish支持许多类型的验证点:验证对象属性具有特定值的验证点——称为“对象属性验证”;验证整个表具有我们期望的内容的验证点——称为“表验证”;验证两张图片是否匹配的验证点——称为“截图验证”;以及包含多个对象的属性和截图的混合验证类型,称为“视觉验证”。此外,还可以验证某个搜索图像是否在屏幕的某个地方存在,或者通过OCR找到某些文本。最常用的类型是对象属性验证,这就是我们在教程中要介绍的内容。有关进一步阅读,请参阅如何创建和使用验证点。
(非脚本化的)常规属性验证点以XML文件形式存储在测试用例或测试套件资源中,并包含需要传递给test.compare()
的值。这些验证点可以在测试用例之间重复使用,并在单个脚本代码行中验证许多值。
Scriptified 属性验证点直接调用 布尔型 test.compare(value1, value2) 函数,其中包含两个参数——特定对象的特定属性的值和预期值。我们可以在记录的或手写的脚本中手动插入 布尔型 test.compare(value1, value2) 函数的调用,或者我们可以通过脚本化验证点让 Squish 自动插入它们。在前一节中,我们展示了如何使用 squishide
在记录过程中插入验证。在这里,我们首先将展示如何使用 squishide
将验证插入到现有的测试脚本中,然后我们将展示如何手动插入验证。
在让 Squish 插入验证点之前,最好确保我们有一个我们想要验证的内容和时间的清单。我们可以在测试案例中添加许多潜在的验证,但鉴于我们在这里的目标只是展示如何操作,我们只会做两个——我们将验证 "Jane Doe" 项的电子邮件地址和电话号码是否符合输入,并将这些验证立即放在我们在记录期间插入的验证之前。
要使用 squishide
插入一个验证点,我们首先在脚本中设置一个断点(无论是记录的还是手工编写的——对 Squish 来说不重要),在想要验证的点。
如上截图所示,我们在第 22 行设置了一个断点。这可以通过双击或右键单击编辑器旁边的空白区域(行号旁)并选择 添加断点 菜单项来实现。我们选择这一行,因为它是之前记录的验证点之前的位置。截图显示了在记录期间使用 squishide
输入的验证。我们的附加验证将出现在它们之前。(注意,如果您的测试是通过不同的方式记录的,例如,使用键盘快捷键而不是点击菜单项,则您的行号可能不同。)
设置断点后,我们通过单击 运行测试 () 或单击 运行 > 运行测试案例 菜单项来按常规方式运行测试。与正常测试运行不同的是,测试将在达到断点时停止(即在 12 行,或者您设置的任何行),Squish 的主窗口将重新出现(这可能会导致 AUT 模糊不清)。此时,squishide
将自动切换到 测试调试视角。
视角和视图
squishide
的工作方式与 Eclipse IDE 类似。如果您不熟悉 Eclipse,理解以下关键概念至关重要: 视图 和 视角。在 Eclipse 中,因此在 squishide
中,一个 视图 实质上是一个子窗口,例如一个停靠窗口或一个现有窗口中的选项卡。一个 视角 是一组排列在一起的视图。两者都可通过 窗口 菜单访问。
squishide
提供以下视角
您可以通过更改这些视角来显示更多的视图或隐藏不需要的视图,或者创建包含您想要的视图的个性化视角。因此,如果您的窗口发生了巨大的变化,这只意味着视角发生了变化。使用窗口菜单回到您想要的视角。然而,Squish会自动调整视角以反映当前情况,所以您通常不需要手动更改视角。
插入验证点
如下面的截图所示,当Squish在断点处停止时,squishide
会自动切换到测试调试视角。该视角显示变量视图、编辑器视图、调试视图、应用程序对象视图和属性视图,以及方法视图和测试结果视图。
要插入验证点,我们可以展开应用程序对象视图中的条目,直到找到我们想要验证的对象,或者我们可以使用对象选择器()工具栏按钮,以在AUT中可视化地选择相关对象。
正常的测试管理视角可以在任何时候通过从窗口菜单中选择它返回(或点击其工具栏按钮)返回,尽管如果停止脚本或运行到完成,squishide
会自动返回到此视角。
在这个例子中,我们想要验证新行的电子邮件地址和电话号码(因为我们已经对名字和姓氏进行了验证)。在AUT中找到它们比在应用程序对象视图中进行导航更容易。首先点击对象选择器()工具栏按钮,然后在AUT中点击新行的电子邮件条目(即,“[email protected]”)。现在,在squishide
中的属性视图中检查innerText
属性。
此时,验证点还没有被添加到测试脚本中。我们可以轻松通过点击保存并插入验证按钮来添加它。但在做这件事之前,我们还将添加一个要验证的内容。
点击对象选择器()然后点击新行的电话号码。现在在squishide
中的属性视图中检查innerText
属性。现在,两个验证都会在验证点创建器视图中显示,如截图所示。
我们现在说我们期望这些属性具有显示的值,即电子邮件地址为“[email protected]”和电话号码为“555 123 4567”。我们必须点击插入按钮来实际插入验证点,所以现在就做吧。
我们不需要继续运行测试,所以我们可以通过点击停止工具栏按钮在本点停止运行测试,或者我们可以继续(通过点击恢复按钮)。
一旦我们完成插入验证并停止或结束运行测试,应现在禁用断点。只需右键单击断点,然后在上下文菜单中点击禁用断点菜单选项。我们现在可以运行测试,没有任何断点但设置了验证点。点击运行测试()。这一次,我们将获得一些额外的测试结果——如截图所示,我们将其中之一展开以显示其详细信息。(我们还选定了Squish插入以执行验证的代码行——请注意,代码的结构与录制时插入的代码结构相同。)
这些特定的验证点会生成四个测试,比较新插入条目的姓名、姓氏、电子邮件和电话号码。
另一种插入验证点的方法是将它们写入代码。从理论上讲,我们可以在现有脚本的任何地方添加对Squish的test
函数的调用,例如布尔测试.compare(value1, value2)和布尔测试.verify(condition)。在实践中,最好确保Squish首先知道我们想要验证的对象,以便在测试运行时找到它们。这涉及到一个与使用squishide
插入非常相似的过程。
- 在打算添加验证的地方设置断点。
- 运行测试脚本直到它停止。
- 在应用程序对象视图中导航,直到找到我们想要验证的对象——或者使用对象选择器()在AUT中视觉上选择它。
- 右键单击我们感兴趣的对象,并点击上下文菜单中的添加到对象映射选项,以确保Squish可以访问该对象。
- 再次右键单击,并点击上下文菜单中的复制符号名选项——这将给我们Squish用来识别对象的名称。
现在我们可以编辑测试脚本以添加自己的验证,并完成或停止执行。不要忘了,一旦不再需要断点就禁用它。
尽管我们可以编写测试脚本代码使其完全与自动生成的代码风格相同,但通常以稍微不同的风格做事会更清晰、更容易,我们将在下一刻解释。
对于我们的手动验证,我们想在加载初始地址后,然后在新地址添加后,最后在第二个地址删除后检查<table>
中现有的地址数量。
原始测试记录没有在Squish的对象映射中存储<table>
的详细信息,因为这些信息对我们记录的测试来说不是必需的。但为了统计行数,我们必须能够访问到表格。这很容易做到。首先,我们在一行代码上设置一个断点(哪一行都可以——只要在断点发生时显示地址列表——我们使用了第5行)。然后,运行测试,当测试被断点停止时,我们点击应用对象视图的对象选择器()工具栏按钮。然后,我们点击AUT中的整个表格。(这可能会有些棘手——只需悬停在红色轮廓包括整个表格为止。)回到squishide
中,我们右击squishide
中的表格,点击应用对象视图中的添加到对象映射上下文菜单选项。然后,再次右击并点击上下文菜单中的复制符号名选项—然后将此名称粘贴到测试脚本中,它将成为numberOfRows
函数的一部分。(别忘了之后移除断点,因为它不再需要了。)
下面的截图显示了我们输入以获取这三个验证之一;它还显示了运行测试脚本的结果。(我们将在稍后看到这个微小的自定义numberOfRows
函数。)
以下是手动为Squish支持的所有脚本语言输入的第一种验证的代码。自然,你只需查看你将用于你自己的测试的代码。对于我们所做的所有行数验证,都调用了Boolean test.verify(condition)函数——或对Tcl来说更为方便的Boolean test.compare(value1, value2)函数。
test.compare(numberOfRows(), 125)
test.compare(numberOfRows(), 125)
test::compare(numberOfRows(), 125);
Test.compare(numberOfRows, 125)
test compare [numberOfRows] 125
下面是我们验证所依赖的自定义numberOfRows
函数。
def numberOfRows(): table = waitForObject(names.dOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) results = table.evaluateXPath(".//TR[contains(@class, 'jqgrow')]") return results.snapshotLength
function numberOfRows() { var table = waitForObject(names.dOCUMENTHTML1BODY1DIV1DIV2DIV3DIV3DIV1TABLE1); var results = table.evaluateXPath(".//TR[contains(@class, 'jqgrow')]"); return results.snapshotLength; }
sub numberOfRows { my $table = waitForObject($Names::document_html1_body1_div1_div2_div3_div3_div1_table1); my $results = $table->evaluateXPath(".//TR[contains(\@class, 'jqgrow')]"); return $results->snapshotLength; }
def numberOfRows table = waitForObject(Names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) results = table.evaluateXPath(".//TR[contains(@class,'jqgrow')]") results.snapshotLength end
proc numberOfRows {} { set table [waitForObject \ $names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1] set results [invoke $table evaluateXPath {.//TR[contains(@class,'jqgrow')]}] return [property get $results snapshotLength] }
在手动编写脚本时,我们使用Squish的test
模块的函数来验证测试脚本执行过程中的一些特定点的条件。如图表(和代码片段)所示,我们首先获取我们感兴趣的对象的引用。对于手动编写的测试脚本,使用Object waitForObject(objectOrName)函数是一种标准做法。该函数等待对象可用(即可见且启用),然后返回它的引用。(否则它会超时并抛出一个可捕获的异常。)然后,我们使用这个引用来访问这个项的属性。在这种情况下,我们获取<table>
的引用并执行一个XPath查询(见如何使用XPath)。这里使用的查询是找到给定元素(即表格)下所有具有包含“jqgrow”值的类属性的<tr>
标签。这会产生一个包含所有匹配的<tr>
的HTML_XPathResult Class对象。因为每个地址都有一个单一的行,所以我们知道具有类"jqgrow"的<tr>
标签的数量等于行数,所以我们只需简单地使用它的HTML_XPathResult.snapshotLength属性来返回项目数量。
要查看更多关于手动编写测试的示例,请参阅手工创建测试、如何创建测试脚本以及如何测试应用程序 - 专用。
有关验证点的全面覆盖,请参阅如何创建和使用验证点。
测试结果
每次测试运行结束后,测试结果(包括验证点的测试结果)将在代码块的底部显示在测试结果视图中。
这是一份详细的测试运行报告,它还将包含任何失败或错误等详细信息。如果您单击测试结果项,代码块将突出显示生成测试结果的脚本行。如果您展开测试结果项,您还可以看到测试的更多详细信息。
Squish的测试结果界面非常灵活。通过实现自定义报告生成器,可以将测试结果以多种方式进行处理的,例如将它们存储在数据库中,或将它们输出为HTML文件。默认的报告生成器在从命令行运行Squish时将结果简单地打印到标准输出,或在使用代码块时输出到测试结果视图。您可以通过右键单击测试结果并选择导出结果菜单选项从代码块保存测试结果作为XML。有关报告生成器的列表,请参阅squishrunner –reportgen: 生成报告。还有可能将测试结果直接记录到数据库中。有关如何从Squish测试脚本访问数据库的详细信息,请参阅如何从Squish测试脚本访问数据库。
如果您使用squishrunner在命令行上运行测试,您还可以以不同的格式导出结果并将它们保存到文件中。有关更多信息,请参阅处理测试结果和如何使用测试语句部分。
手工创建测试
我们已经看到了如何记录测试并通过插入验证点来修改它,现在我们准备了解如何手动创建测试。最简单的方法是修改和重构已记录的测试,虽然从头开始创建手动测试也是完全可能的。
编写手动测试最具挑战性的部分可能是有合适的目标名称,但在实践中,这很少成为问题。我们可以复制Squish在记录之前的测试时已经添加到对象映射中的符号名称,或者我们可以直接从记录的测试中复制目标名称。如果没有记录任何测试并从头开始,我们可以使用间谍工具。我们这样做是通过单击工具栏上的启动AUT按钮。这会启动AUT并切换到间谍视角。然后我们可以与AUT进行交互,直到我们感兴趣的对象可见。然后,在代码块中,我们可以导航到应用程序对象视图中的对象,或者使用对象选择器( )工具栏按钮,并使用上下文菜单将对象添加到对象映射中(以便Squish记住它),并将其添加到剪贴板(以便我们可以将其粘贴到我们的测试脚本中)。最后,我们可以单击工具栏上的退出AUT按钮来终止AUT并将Squish返回到测试管理视角。有关使用间谍工具的详细信息,请参阅如何使用间谍工具。
我们可以通过点击工具栏中的对象映射按钮来查看对象映射(另请参阅,对象映射视图)。Squish与之交互的每个应用程序对象都列在这里,无论是作为顶级对象,还是作为子对象(视图是一个树形视图)。我们可以通过右键点击我们感兴趣的对象,然后点击上下文菜单中的“复制”项来检索Squish在录制脚本中使用的符号名称。这对于我们想要修改现有测试脚本或当我们想从头开始创建测试脚本时很有用,正如我们在教程后面将看到的。
修改和重构录制测试
假设我们想要通过添加三个新的姓名和地址来测试AUT的添加功能。我们可以录制这样的测试,但完全在代码中进行也同样简单。我们需要测试脚本执行的步骤是:启动应用程序,然后对于每个新的姓名和地址,点击添加按钮,填写详细信息,然后点击保存。我们还想在点击新按钮后验证没有数据行,并在最后验证有三行。我们将边做边重构,尽可能使代码整洁且模块化。
首先,我们必须创建一个新的测试用例。点击文件 > 新建测试用例,将测试用例的名称设置为tst_adding
。Squish将自动创建test.js
(或test.py
等)文件。
命令行用户可以在测试套件的目录下简单创建一个名为tst_adding
的新目录,并在该目录下创建和编辑test.js
文件(或test.py
等)。
我们首先需要一种启动AUT的方法。以下是来自已录制tst_general脚本的前几行
import names def main(): startBrowser("http://localhost:9090/AddressBook.html")
import * as names from 'names.js'; function main() { startBrowser("http://localhost:9090/AddressBook.html");
require 'names.pl'; sub main { startBrowser("http://localhost:9090/AddressBook.html");
require 'squish' require 'names' include Squish def main startBrowser("http://localhost:9090/AddressBook.html")
source [findFile "scripts" "names.tcl"] proc main {} { invoke startBrowser "http://localhost:9090/AddressBook.html"
请注意,代码中的模式很简单:首先导入对象映射,然后在main
中启动AUT,然后等待页面准备就绪。
注意:将函数放入tst_adding
中可能看起来有些浪费,因为我们也可以在tst_general
和其他测试用例中使用它们。然而,为了保持教程的简单性,我们将代码放在tst_adding
测试用例中。有关如何共享脚本,请参阅如何创建和使用共享数据和共享脚本。
如果在测试执行期间AUT似乎冻结,等待Squish超时AUT(大约20秒),并显示对象未找到对话框,指示此错误
这通常意味着Squish没有在对象映射中找到给定名称的对象或属性值。从这里,我们可以选择新对象、调试、抛出错误,或在选取新对象后重试。
选择新对象将更新符号名称的对象映射条目。除了对象选择器()之外,我们还可以使用Spy的应用程序对象视图来定位感兴趣的对象,并使用添加到对象映射上下文菜单操作来访问它们的真实或符号名称。
命名很重要,因为这可能是编写脚本导致的最多的错误消息的部分,通常是上述对象...未找到类型的错误。一旦我们确定了在测试中要访问的对象,使用Squish编写测试脚本就非常简单。特别是,因为Squish很可能支持您最熟悉的脚本语言。
我们现在几乎准备好编写自己的测试脚本了。最简单的方法是从录制一个测试样例开始。因此,点击文件 > 新建测试用例并将测试用例的名称设置为tst_dummy
。然后单击测试样例的录制()。一旦AUT启动,点击新建按钮,然后点击确定。这将清除示例数据并使表格为新的数据准备好。点击控制栏窗口的停止录制按钮。仅为了确认一切正常,回放这个测试。这样做的主要目的是确保Squish将必要的名称添加到对象映射中,因为这样做可能比为每个感兴趣的对象使用间谍程序更快。在回放下一个测试样例后,您可以删除它,如果需要的话。
有了对象映射中所有我们需要的对象名称,我们现在可以从头开始编写自己的测试脚本。我们将从main
函数开始,然后我们将查看main
函数使用的辅助函数。
import names def main(): startBrowser("http://localhost:9090/AddressBook.html") confirmPopup(names.newButton_button) test.verify(numberOfRows() == 0, "%d" % numberOfRows()) 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(numberOfRows(), 3)
import * as names from 'names.js'; function main() { startBrowser("http://localhost:9090/AddressBook.html"); confirmPopup(names.newButtonButton); test.verify(numberOfRows() == 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(numberOfRows(), 3); }
require 'names.pl'; sub main { startBrowser("http://localhost:9090/AddressBook.html"); confirmPopup($Names::newbutton_button); test::verify(numberOfRows() == 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 $oneNameAndAddress (@data) { addNameAndAddress(@{$oneNameAndAddress}); } test::compare(numberOfRows(), 3); }
require 'names'; # encoding: UTF-8 require 'squish' include Squish def main startBrowser("http://localhost:9090/AddressBook.html") confirmPopup(Names::NewButton_button) Test.verify(numberOfRows == 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(numberOfRows, 3) end
source [findFile "scripts" "names.tcl"] proc main {} { invoke startBrowser "http://localhost:9090/AddressBook.html" confirmPopup $names::newButton_button test compare [numberOfRows] 0 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 [numberOfRows] 3 }
我们首先使用带有我们要它启动的网页名称的startBrowser(url)
函数调用AUT开始。这意味着Squish不会要求我们确认要加载的页面。接下来,我们调用一个自定义的confirmPopup
函数来点击新建(这会打开一个OK/Cancel对话框),并确认(即点击确定)。这将使表格变空。(我们从对象映射中复制了新建按钮的名称——这是由我们记录的测试样例放置的。)接下来,我们调用我们之前创建的自定义numberOfRows
函数来验证表格为空。
接下来,我们创建一些示例数据,并调用自定义的addNameAndAddress
函数使用AUT的添加对话框将数据填充到表格中。最后,我们再次比较表格的行数,这一次是比较示例数据中的行数。
我们现在将讨论三个辅助函数中的两个,以便涵盖tst_adding
测试用例中的所有代码,首先是confirmPopup
函数。(第三个函数numberOfRows
在前面已经讨论过。)
def confirmPopup(button): clickButton(waitForObject(button)) snooze(1.8) closeConfirm(names.confirmPopup, True)
function confirmPopup(button) { clickButton(waitForObject(button)); snooze(1.8); closeConfirm(names.confirmPopup, true); }
sub confirmPopup { my ($button) = @_; clickButton(waitForObject($button)); snooze(1.8); closeConfirm($Names::confirmpopup, 1); }
def confirmPopup(button) clickButton(waitForObject(button)) snooze(1.8) closeConfirm(Names::ConfirmPopup, true) end
proc confirmPopup {button} { invoke clickButton [waitForObject $button] snooze 1.8 invoke closeConfirm $names::ConfirmPopup true }
这个函数使用我们想要点击并确认的按钮的符号名调用来调用。(名称是从对象映射中复制的。)点击后,我们使用snooze(seconds)
函数强制Squish等待一段短时间(1.8秒),然后使用closeConfirm(":dummy", confirmed)
函数关闭弹出的对话框。第一个参数可以是任何文本,但第二个必须是true
(这意味着点击确定)或false
(这意味着点击取消)。所以这里我们点击了确定。
def addNameAndAddress(oneNameAndAddress): clickButton(waitForObject(names.addButton_button)) typeText(waitForObject(names.oneitem_forenameEdit_text), oneNameAndAddress[0]) typeText(waitForObject(names.oneitem_surnameEdit_text), oneNameAndAddress[1]) typeText(waitForObject(names.oneitem_emailEdit_text), oneNameAndAddress[2]) typeText(waitForObject(names.oneitem_phoneEdit_text), oneNameAndAddress[3]) clickButton(waitForObject(names.save_button))
function addNameAndAddress(oneNameAndAddress) { clickButton(waitForObject(names.addButtonButton)); typeText(waitForObject(names.froglogicAddressbookForenameEditText), oneNameAndAddress[0]); typeText(waitForObject(names.froglogicAddressbookSurnameEditText), oneNameAndAddress[1]); typeText(waitForObject(names.froglogicAddressbookEmailEditText), oneNameAndAddress[2]); typeText(waitForObject(names.froglogicAddressbookPhoneEditText), oneNameAndAddress[3]); clickButton(waitForObject(names.saveButton)); }
sub addNameAndAddress { my (@oneNameAndAddress) = @_; clickButton(waitForObject($Names::addbutton_button)); typeText(waitForObject($Names::oneitem_forenameedit_text), $oneNameAndAddress[0]); typeText(waitForObject($Names::oneitem_surnameedit_text), $oneNameAndAddress[1]); typeText(waitForObject($Names::oneitem_emailedit_text), $oneNameAndAddress[2]); typeText(waitForObject($Names::oneitem_phoneedit_text), $oneNameAndAddress[3]); clickButton(waitForObject($Names::save_button)); }
def addNameAndAddress(oneNameAndAddress) clickButton(waitForObject(Names::AddButton_button)) typeText(waitForObject(Names::Oneitem_forenameEdit_text), oneNameAndAddress[0]) typeText(waitForObject(Names::Oneitem_surnameEdit_text), oneNameAndAddress[1]) typeText(waitForObject(Names::Oneitem_emailEdit_text), oneNameAndAddress[2]) typeText(waitForObject(Names::Oneitem_phoneEdit_text), oneNameAndAddress[3]) clickButton(waitForObject(Names::Save_button)) end
proc addNameAndAddress {oneNameAndAddress} { invoke clickButton [waitForObject $names::addButton_button] invoke typeText [waitForObject $names::oneitem_forenameEdit_text] [lindex $oneNameAndAddress 0] invoke typeText [waitForObject $names::oneitem_surnameEdit_text] [lindex $oneNameAndAddress 1] invoke typeText [waitForObject $names::oneitem_emailEdit_text] [lindex $oneNameAndAddress 2] invoke typeText [waitForObject $names::oneitem_phoneEdit_text] [lindex $oneNameAndAddress 3] invoke clickButton [waitForObject $names::Save_button] }
对于每一组姓名和地址数据,我们点击添加按钮使添加表单可见。然后对于收到的每个值,我们通过等待相关的text
字段准备就绪,然后使用setText(objectOrName, text)函数输入文本。最后,我们点击表单的保存按钮。我们得到函数核心的代码是通过复制它从记录的tst_general
测试中得来的,并简单地通过字段名和文本进行参数化。同样,我们从tst_general
测试用例的代码中复制了点击保存按钮的代码。
整个测试代码大约有35行——如果我们把一些通用函数(如 confirmPopup
和 numberOfRows
)放在一个共享脚本中,那么行数会更少。大多数代码都是从录制的测试中直接复制过来的,有些情况下是进行了参数化。
这应该足以让你了解如何编写自动化测试(AUT)的测试脚本。请注意,Squish提供的功能远远不止这里用到的(所有这些都在 API参考手册 和 工具参考手册 中有详细说明)。Squish 还提供了对 AUT 对象的所有公共 API 的访问。
然而,测试用例的某个方面并不令人满意。虽然像我们这里这样嵌入测试数据在数量小的情况下是合理的,但它相当有限,尤其是当我们想使用大量测试数据时。此外,我们没有测试添加的数据是否会正确地出现在表中。在下一节中,我们将创建这个测试的新版本,但这一次我们将从外部数据源获取数据,并检查添加到表中的数据是否正确。
创建数据驱动测试
在上一节中,我们在测试中放入了三个硬编码的名称和地址。但是如果我们想测试大量的数据呢?或者如果我们想更改数据而不必更改测试脚本源代码,该怎么办?一种方法是将数据集导入 Squish,并使用数据集作为我们测试中插入值的来源。Squish 可以导入 .tsv
(制表符分隔值格式)、.csv
(逗号分隔值格式)、.xls
或 .xlsx
(Microsoft Excel 工作表格式)的数据。
注意:假设 .csv
和 .tsv
文件使用 Unicode UTF-8 编码——与所有测试脚本相同的编码。
测试数据可以借助 squishide
或使用文件管理器或控制台命令手动导入。我们将描述两种方法,从使用 squishide
开始。
对于地址簿应用程序,我们想导入名为 MyAddresses.tsv
的数据文件。为此,我们必须点击 文件 > 《b translate="no">导入测试资源 以弹出 导入 Squish 资源对话框。在对话框中,点击 浏览 按钮选择要导入的文件——在本例中是 MyAddresses.tsv
。确保将 导入为 组合框设置为 "测试数据"。默认情况下,squishide
只会将测试数据导入当前测试用例,但我们要使测试数据对所有测试套件中的测试用例都可用:为此请选中 复制到测试套件以共享 单选按钮。现在点击 完成 按钮即可。你现在可以在测试套件资源视图(在测试数据选项卡中)中看到该文件,如果您点击该文件的名称,它将在一个 编辑器视图中显示。截图显示了添加了测试数据后的 Squish。
要从 squishide
外部导入测试数据,请使用文件管理器,如文件资源管理器或 Finder,或者使用控制台命令。在测试套件目录中创建一个名为 shared
的目录。然后在 shared
目录内创建一个名为 testdata
的目录。将数据文件(在本例中为 MyAddresses.tsv
)复制到 shared\testdata
目录中。
如果它正在运行,请重新启动 squishide
。如果您点击 测试套件资源 视图的 测试数据 选项卡,您应该可以看到数据文件。点击文件名可以在 编辑器视图 中查看文件。
虽然在现实生活中我们会修改我们的 tst_adding
测试用例以使用测试数据,但为了教程的目的,我们将创建一个新的测试用例,称为 tst_adding_data
,它是 tst_adding
的副本,我们将修改它以使用测试数据。
我们必须更改的唯一函数是 main
,在这里我们不是遍历硬编码的数据项,而是遍历数据集中的所有记录。现在我们添加了更多记录,我们还需要更新预期的行数,并且我们将添加一个函数来验证添加的每个记录。
import names def main(): startBrowser("http://localhost:9090/AddressBook.html") confirmPopup(names.newButton_button) test.verify(numberOfRows() == 0) limit = 10 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)) # pass as a single tuple checkNameAndAddress(record) if row > limit: break test.compare(numberOfRows(), row + 1)
import * as names from 'names.js'; function main() { startBrowser("http://localhost:9090/AddressBook.html"); confirmPopup(names.newButtonButton); test.verify(numberOfRows() == 0); var limit = 10; 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)); checkNameAndAddress(record); if (row > limit) break; } test.compare(numberOfRows(), row + 1); }
require 'names.pl'; sub main { startBrowser("http://localhost:9090/AddressBook.html"); confirmPopup($Names::newbutton_button); test::verify(numberOfRows() == 0); my @records = testData::dataset("MyAddresses.tsv"); my $limit = 10; 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)); checkNameAndAddress($record); if ($row > $limit) { last; } } test::compare(numberOfRows(), $row + 1); }
require 'squish' require 'names'; include Squish def main startBrowser("http://localhost:9090/AddressBook.html") confirmPopup(Names::NewButton_button) Test.verify(numberOfRows == 0) limit = 10 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]) # pass as an Array checkNameAndAddress(record) break if row > limit rows += 1 end Test.compare(numberOfRows, rows + 1) end
source [findFile "scripts" "names.tcl"] proc main {} { invoke startBrowser "http://localhost:9090/AddressBook.html" confirmPopup $names::newButton_button test compare [numberOfRows] 0 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 checkNameAndAddress $record if {$row > $limit} { break } } test compare [numberOfRows] [expr $row + 1] }
Squish 通过它的 testData
模块的函数提供对测试数据的访问——在这里我们使用了 Dataset testData.dataset(filename) 函数来访问数据文件并将其记录提供,使用了 String testData.field(record, fieldName) 函数来检索每个记录的各个字段。
我们使用测试数据填充了我们想要的HTML表,因此我们确信表中的数据与我们添加的是相同的,这就是为什么我们添加了 checkNameAndAddress
函数。我们还添加了一个比较记录数的限制,以使测试运行更快。
def checkNameAndAddress(record): table = waitForObject(names.dOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) cells = (table.evaluateXPath(".//TR/TD[2]"), table.evaluateXPath(".//TR/TD[3]"), table.evaluateXPath(".//TR/TD[4]"), table.evaluateXPath(".//TR/TD[5]")) for column in range(len(testData.fieldNames(record))): cell = cells[column].snapshotItem(cells[column].snapshotLength-1).innerText field = testData.field(record, column) test.compare(cell, field)
function checkNameAndAddress(record) { var table = waitForObject(names.dOCUMENTHTML1BODY1DIV1DIV2DIV3DIV3DIV1TABLE1); var cells = [table.evaluateXPath(".//TR/TD[2]"), table.evaluateXPath(".//TR/TD[3]"), table.evaluateXPath(".//TR/TD[4]"), table.evaluateXPath(".//TR/TD[5]")]; for (var column = 0; column < testData.fieldNames(record).length; ++column) { var cell = cells[column].snapshotItem(cells[column].snapshotLength-1).innerText; var field = testData.field(record, column); test.compare(cell, field); } }
sub checkNameAndAddress { my ($record) = @_; my $table = waitForObject($Names::document_html1_body1_div1_div2_div3_div3_div1_table1); my @cells = ($table->evaluateXPath(".//TR/TD[2]"), $table->evaluateXPath(".//TR/TD[3]"), $table->evaluateXPath(".//TR/TD[4]"), $table->evaluateXPath(".//TR/TD[5]")); my @columnNames = testData::fieldNames($record); for (my $column = 0; $column < scalar(@columnNames); ++$column) { my $cell = $cells[$column]->snapshotItem($cells[$column]->snapshotLength - 1)->innerText; my $field = testData::field($record, $column); test::compare($cell, $field); } }
def checkNameAndAddress(record) table = waitForObject(Names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) cells = [table.evaluateXPath(".//TR/TD[2]"), table.evaluateXPath(".//TR/TD[3]"), table.evaluateXPath(".//TR/TD[4]"), table.evaluateXPath(".//TR/TD[5]")] for column in 0...TestData.fieldNames(record).length cell = cells[column].snapshotItem(cells[column].snapshotLength-1).innerText field = TestData.field(record, column) Test.compare(cell, field) end end
proc checkNameAndAddress {record} { set table [waitForObject $names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1] set cells [list \ [invoke $table evaluateXPath {.//TR/TD[2]}] \ [invoke $table evaluateXPath {.//TR/TD[3]}] \ [invoke $table evaluateXPath {.//TR/TD[4]}] \ [invoke $table evaluateXPath {.//TR/TD[5]}]] set columns [llength [testData fieldNames $record]] for {set column 0} {$column < $columns} {incr column} { set itemPos [expr [property get [lindex $cells $column] snapshotLength ] - 1 ] set cell [property get [invoke [lindex $cells $column] \ snapshotItem $itemPos] innerText] set field [testData field $record $column] test compare $cell $field } }
调用 HTML_XPathResult HTML_Object.evaluateXPath(statement) 函数使用XPath查询来访问HTML表并找到匹配的第一个单元格。由于地址簿应用程序总是将新地址添加到末尾,因此代码需要使用查询中的最后一个项。我们使用Squish的 SequenceOfStrings testData.fieldNames(record) 函数获取列数,然后使用 Boolean test.compare(value1, value2) 函数来检查表中每个值是否与我们所使用的测试数据中的值相同。
截图显示了数据驱动测试运行后的Squish测试摘要日志。
Squish还可以进行关键字驱动测试。这比数据驱动测试复杂一些。请参阅 如何进行关键字驱动测试。
了解更多
我们现在已经完成了教程。Squish可以做到比我们在这里展示的更多,但目标是以尽可能快和简单的方式让您开始基本的测试。有关如何创建测试脚本(How to Create Test Scripts)和相关工具包特定APIs(如何测试应用程序 - 专用)的部分提供了更多的例子,包括那些显示测试如何与特定输入元素交互的例子,例如选择、选择项、文本和文本区域。
《API参考手册》(API参考)和《工具参考手册》(工具参考)详细介绍了Squish的测试API及其提供的众多功能,旨在使测试尽可能容易和高效。阅读《如何创建测试脚本》和《如何测试应用程序 - 特殊之处》以及浏览《API参考》和《工具参考》是非常值得的。你投入的时间将会得到回报,因为你将知道Squish提供的是前所未有的功能,并可以避免重新发明已经存在的东西。
教程:设计行为驱动开发(BDD)测试
本教程将向您展示如何为示例应用程序创建、运行和修改行为驱动开发(BDD)测试。您将了解Squish最常用的功能。教程结束后,您将能够编写自己的测试脚本。
在本章中,我们将使用一个简单的地址簿应用程序作为我们的AUT。该应用程序包含在Squish中,位于SQUISHDIR/examples/web/addressbook
。这是一个非常基础的程序,允许用户与模拟的现有地址簿交互或创建一个新的,并添加、编辑和删除条目。尽管应用程序很简单,但它具有大多数标准Web应用程序的关键功能:按钮、单选按钮、行编辑、弹出对话框和一个中央区域——在这个例子中显示一个表格。您学到测试此应用程序的所有想法和实践都可以轻松地应用到您的应用程序中。有关测试各种Web特定功能和标准编辑小部件的更多示例,请参阅《如何创建测试脚本》和《如何测试Web应用程序》。
截图显示了应用程序的操作。
添加新地址。
使用示例
本教程的示例是一个包含在文件SQUISHDIR/examples/web/addressbook/AddressBook.html
中的HTML和JavaScript Web应用。Squish for Web旨在测试通过Web服务器在http:上提供的真实Web应用,并且AddressBook示例需要在至少一个Web服务器上托管。
服务器是用Python编写的;只需使用系统上的Python解释器,或在SQUISHDIR
下(python2
或python3
)运行它即可。
$ python SQUISHDIR/examples/web/addressbook/server.py
如果您使用的是Windows
C:\> cd SQUISHDIR\examples\web\addressbook C:\SQUISHDIR\examples\web\addressbook> ..\..\..\python2\python server.py
一旦服务器运行,您可以使用URL http://localhost:9090/AddressBook.html
访问Web地址簿示例应用程序。(如果端口9090与您的机器上其他任何东西冲突,只需将未使用的端口号作为命令行参数传递给server.py
,它会使用该端口号。当然,如果使用不同的端口号,在整个教程中必须使用该端口号。)
注意:在Windows上,首次启动任何TCP/IP服务器(包括squishserver或server.py
)时,根据您的安全设置,Windows可能会弹出一个对话框,询问您是否允许或阻止服务器运行。如果您收到此对话框,您必须选择未阻止,以便Squish可以正常运行。
行为驱动开发简介
行为驱动开发(BDD)是测试驱动开发方法的一个扩展,它将验收标准的定义放在开发过程的开头,而不是在软件开发后编写测试。可以在此测试后进行代码更改的循环。
行为驱动测试由一系列Feature
文件构成,这些文件通过一个或多个Scenarios
描述产品功能,这些Scenarios
由一系列动作或需要为此Scenarios
进行验证的步骤组成。
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 | forename | surname | email | phone | | John | Smith | john@m.com | 1231231 | | Alice | Thomson | alice@m.com | 2342342 | 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
上述大部分内容是自由文本(不需要是英文)。这只是一个固定的特征
/场景
结构,以及引导关键字如Given
、And
、When
和Then
。每一个关键字标记了一个步骤,定义了先决条件、用户操作或预期结果。上述的应用程序行为描述可以传递给软件开发人员以实现这些功能,同时相同的描述也可以供软件测试人员用来实现自动化测试。
测试实现
创建测试套件
首先,我们需要创建一个测试套件,它是一个所有测试用例的容器。启动 SquishIDE,然后选择 文件 > 新建测试套件。请按照新建测试套件向导的指示,提供测试套件名称,选择您想要的 Web 工具包和脚本语言。有关创建新测试套件的更多详细信息,请参阅创建测试套件。
创建测试用例
Squish 提供两种类型的测试用例:“脚本测试用例”和“BDD 测试用例”。由于“脚本测试用例”是默认的,为了创建新的“BDD 测试用例”,我们需要通过点击 新建脚本测试用例 旁边的展开器( )并选择 新建 BDD 测试用例 来使用下拉菜单。SquishIDE 将记住您的选择,并且在将来点击按钮时,“BDD 测试用例”将成为默认选项。
新建的 BDD 测试用例包含一个 test.feature
文件(在创建新的 BDD 测试用例时填充了 Gherkin 模板),一个名为 test.(py|js|pl)
的文件用于驱动执行(不需要编辑此文件),还有一个名为 steps/steps.(py|js|pl)
的测试套件资源文件,其中将放置步骤实现代码。
我们需要用地址簿示例应用程序的 特征
替换 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
在编辑 test.feature
文件时,每个未定义的步骤都会显示 特征
文件警告“未找到实现”。实现代码位于 steps
子目录中,在 测试用例资源 或 测试套件资源 中。现在运行我们的 特征
测试将当前在第一步失败,显示“未找到匹配的步骤定义”,并且后续步骤将被跳过。
记录步骤实现
要记录 场景
,请点击测试用例资源视图中的“场景”选项卡中列出的 respective 场景
旁边的 记录 按钮。
这将会使 squishide
退出,并弹出对话框询问您 Web 浏览器的起始 URL。输入 http://localhost:9090/AddressBook.html
以启动浏览器。此外,还会显示控制栏,其中包含需要记录的所有步骤。现在所有与 AUT 的交互或添加到脚本的验证点都将记录在第一步 Given 地址簿应用程序正在运行
下(在控制栏的步骤列表中加粗显示)。为了验证这个先决条件,我们将添加一个验证点。为此,点击控制栏中的 验证 并选择 属性。
因此,将 squishide
转换到 Spy 模式,该模式在 squishide
底部的可停靠视图中显示所有 应用程序对象 和 属性。在 应用程序对象 中,选择 DOCUMENT
对象。选择它将更新其右边的 属性 视图。接下来,点击 属性 视图中的属性标题前的复选框。最后,点击 保存并插入验证 按钮。 squishide
将消失,并将再次显示控件栏。
完成每个步骤后,我们可以通过点击位于当前步骤左侧的控件栏中的 完成记录步骤(《》)箭头按钮来移动到下一个未定义的步骤(回放先前定义的步骤)。
接下来,对于步骤 当我创建一个新的地址簿
,点击 新建 按钮,然后点击 完成记录步骤(《》)。
最后,对于步骤 然后地址簿应该为零条条目
,验证包含地址条目的表格是否为空。为了记录此验证,请在录制时点击 验证,选择 属性,然后在 应用程序对象 视图中选择表格的主体。请看以下 tbody
元素的位置,因为它嵌套较深。从 属性 视图中检查 numChildren 项(它具有值为 1,因为表格中始终有一个额外的元素)。然后,点击 保存并插入验证。最后,点击控件栏中最后的 完成记录步骤(《》)箭头按钮。
结果,Squish 将在 测试套件 > 测试套件资源 的 steps.*
文件中生成以下步骤定义。
@Given("addressbook application is running") def step(context): startBrowser("http://127.0.0.1:9090/AddressBook.html") test.compare(waitForObjectExists(names.dOCUMENT).title, "froglogic Addressbook") @When("I create a new addressbook") def step(context): clickButton(waitForObject(names.froglogic_Addressbook_newButton_button)) snooze(2) closeConfirm(names.confirmPopup, True) @Then("addressbook should have zero entries") def step(context): test.compare(waitForObjectExists(names.dOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1).numChildren, 1)
Given("addressbook application is running", function(context) { startBrowser("http://127.0.0.1:9090/AddressBook.html"); test.compare(waitForObjectExists(names.dOCUMENT).title, "froglogic Addressbook"); }); When("I create a new addressbook", function(context) { clickButton(waitForObject(names.froglogicAddressbookNewButtonButton)); snooze(2); closeConfirm(names.confirmPopup, true); }); Then("addressbook should have zero entries", function(context) { test.compare(waitForObjectExists(names.dOCUMENTHTML1BODY1DIV1DIV2DIV3DIV3DIV1TABLE1TBODY1).numChildren, 1); });
Given("addressbook application is running", sub { my $context = shift; startBrowser("http://127.0.0.1:9090/AddressBook.html"); test::compare(waitForObjectExists($Names::document)->title, "froglogic Addressbook"); }); When("I create a new addressbook", sub { my $context = shift; clickButton(waitForObject($Names::froglogic_addressbook_newbutton_button)); snooze(2); closeConfirm($Names::confirmpopup, 1); }); Then("addressbook should have zero entries", sub { my $context = shift; test::compare(waitForObjectExists($Names::document_html1_body1_div1_div2_div3_div3_div1_table1_tbody1)->numChildren, 1); });
Given("addressbook application is running") do |context| startBrowser("http://127.0.0.1:9090/AddressBook.html") Test.compare(waitForObjectExists(Names::DOCUMENT).title, "froglogic Addressbook") end When("I create a new addressbook") do |context| clickButton(waitForObject(Names::Froglogic_Addressbook_newButton_button)) snooze(2) closeConfirm(Names::ConfirmPopup, true) end Then("addressbook should have zero entries") do |context| Test.compare(waitForObjectExists(Names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1).numChildren, 1) end
Given "addressbook application is running" {context} { invoke startBrowser "http://127.0.0.1:9090/AddressBook.html" test compare [property get [waitForObject $names::DOCUMENT] title] "froglogic Addressbook" } When "I create a new addressbook" {context} { invoke clickButton [waitForObject $names::froglogic_Addressbook_newButton_button] snooze 2 invoke closeConfirm $names::ConfirmPopup true } Then "addressbook should have zero entries" {context} { test compare [property get [waitForObjectExists $names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1] numChildren] 1 }
由于记录了 startApplication()
调用,应用程序在第一个步骤的开始处自动启动。在每个场景结束时,将调用 OnScenarioEnd
插件,这会导致在应用程序上下文中调用 detach()
。因为AUT是使用 startApplication()
启动的,这会导致它终止。此插件函数位于 脚本 标签中的 bdd_hooks.(py|js|pl|rb|tcl)
文件中,该标签位于 测试套件资源 视图。您可以在其中定义额外的插件函数。有关所有可用的插件函数列表,请参阅 通过插件在测试执行期间执行操作。
@OnScenarioEnd
def hook(context):
closeWindow(":[Window]");
OnScenarioEnd(function(context) {
closeWindow(":[Window]");
});
OnScenarioEnd(sub {
closeWindow(":[Window]");
});
OnScenarioEnd do |context| closeWindow(":[Window]"); end
OnScenarioEnd { context } {
invoke closeWindow ":\[Window\]"
}
}
步骤参数化
迄今为止,我们的步骤没有使用任何参数,所有值都是硬编码的。Squish 有不同类型的参数,如 any
、integer
或 word
,允许我们的步骤定义更具可重复使用性。让我们在 Feature
文件中添加一个新的 Scenario
,该文件将为测试数据和预期结果提供步骤参数。将以下部分复制到您的 Feature 文件中。
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 |
自动保存 Feature
文件后,《squishide
》将提供一个提示,表示只需实现 2 个步骤:当我向地址簿添加一个新的人 'John','Doe','[email protected]','500600700' 时
和 然后应该有 '1' 个条目
。其他步骤已经有了匹配的步骤实现。
要记录缺失的步骤,请在测试套件视图中点击测试用例名称旁边的“记录”按钮。脚本将播放到缺失步骤,然后提示您实现它。如果您选择“添加”按钮,则可以输入新条目的信息。单击完成记录步骤()按钮以进入下一步。对于第二个缺失步骤,我们可以记录一个对象属性验证,就像我们在步骤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.froglogic_Addressbook_addButton_button)) setText(waitForObject(names.froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(names.froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(names.froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(names.froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(names.froglogic_Addressbook_Save_button))
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) { clickButton(waitForObject(names.froglogicAddressbookAddButtonButton)); mouseClick(waitForObject(names.froglogicAddressbookAddButtonButton)); setText(waitForObject(names.froglogicAddressbookForenameEditText), forename); setText(waitForObject(names.froglogicAddressbookSurnameEditText), surname); setText(waitForObject(names.froglogicAddressbookEmailEditText), email); setText(waitForObject(names.froglogicAddressbookPhoneEditText), phone); clickButton(waitForObject(names.froglogicAddressbookSaveButton));
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::froglogic_addressbook_addbutton_button)); setText(waitForObject($Names::froglogic_addressbook_forenameedit_text), $forename); setText(waitForObject($Names::froglogic_addressbook_surnameedit_text), $surname); setText(waitForObject($Names::froglogic_addressbook_emailedit_text), $email); setText(waitForObject($Names::froglogic_addressbook_phoneedit_text), $phone); clickButton(waitForObject($Names::froglogic_addressbook_save_button));
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone| clickButton(waitForObject(Names::Froglogic_Addressbook_addButton_button)) setText(waitForObject(Names::Froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(Names::Froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(Names::Froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(Names::Froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(Names::Froglogic_Addressbook_Save_button))
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} { invoke clickButton [waitForObject $names::froglogic_Addressbook_addButton_button] invoke setText [waitForObject $names::froglogic_Addressbook_forenameEdit_text] $forename invoke setText [waitForObject $names::froglogic_Addressbook_surnameEdit_text] $surname invoke setText [waitForObject $names::froglogic_Addressbook_emailEdit_text] $email invoke setText [waitForObject $names::froglogic_Addressbook_phoneEdit_text] $phone invoke clickButton [waitForObject $names::froglogic_Addressbook_Save_button]
如果我们将最终的Then
记录为一个缺失步骤,并在
@Then("'|integer|' entries should be present") def step(context, entries): test.compare(waitForObjectExists(names.dOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1).numChildren, entries + 1)
Then("'|integer|' entries should be present", function(context, entries) { test.compare(waitForObjectExists(names.dOCUMENTHTML1BODY1DIV1DIV2DIV3DIV3DIV1TABLE1TBODY1).numChildren, entries + 1); });
Then("'|integer|' entries should be present", sub { my $context = shift; my $entries = shift; test::compare(waitForObjectExists($Names::document_html1_body1_div1_div2_div3_div3_div1_table1_tbody1)->numChildren, $entries + 1); });
Then("'|integer|' entries should be present") do |context, entries| Test.compare(waitForObjectExists(Names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1).numChildren, entries + 1) end
Then "'|integer|' entries should be present" {context entries} { test compare [property get [waitForObjectExists $names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1_TBODY1] numChildren] [expr $entries + 1] }
在表中为步骤提供参数
下一个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 | 1231231 | | Alice | Thomson | alice@m.com | 2342342 | 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.froglogic_Addressbook_addButton_button)) setText(waitForObject(names.froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(names.froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(names.froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(names.froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(names.froglogic_Addressbook_Save_button))
When("I add new persons to address book", function(context) { var table = context.table; // Skip initial row with column headers by starting at index 1 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.froglogicAddressbookAddButtonButton)); mouseClick(waitForObject(names.froglogicAddressbookAddButtonButton)); setText(waitForObject(names.froglogicAddressbookForenameEditText), forename); setText(waitForObject(names.froglogicAddressbookSurnameEditText), surname); setText(waitForObject(names.froglogicAddressbookEmailEditText), email); setText(waitForObject(names.froglogicAddressbookPhoneEditText), phone); clickButton(waitForObject(names.froglogicAddressbookSaveButton)); } });
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::froglogic_addressbook_addbutton_button)); setText(waitForObject($Names::froglogic_addressbook_forenameedit_text), $forename); setText(waitForObject($Names::froglogic_addressbook_surnameedit_text), $surname); setText(waitForObject($Names::froglogic_addressbook_emailedit_text), $email); setText(waitForObject($Names::froglogic_addressbook_phoneedit_text), $phone); clickButton(waitForObject($Names::froglogic_addressbook_save_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 clickButton(waitForObject(Names::Froglogic_Addressbook_addButton_button)) setText(waitForObject(Names::Froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(Names::Froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(Names::Froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(Names::Froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(Names::Froglogic_Addressbook_Save_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::froglogic_Addressbook_addButton_button] invoke setText [waitForObject $names::froglogic_Addressbook_forenameEdit_text] $forename invoke setText [waitForObject $names::froglogic_Addressbook_surnameEdit_text] $surname invoke setText [waitForObject $names::froglogic_Addressbook_emailEdit_text] $email invoke setText [waitForObject $names::froglogic_Addressbook_phoneEdit_text] $phone invoke clickButton [waitForObject $names::froglogic_Addressbook_Save_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.froglogic_Addressbook_addButton_button)) setText(waitForObject(names.froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(names.froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(names.froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(names.froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(names.froglogic_Addressbook_Save_button)) # save userData for last step 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.froglogicAddressbookAddButtonButton)); mouseClick(waitForObject(names.froglogicAddressbookAddButtonButton)); setText(waitForObject(names.froglogicAddressbookForenameEditText), forename); setText(waitForObject(names.froglogicAddressbookSurnameEditText), surname); setText(waitForObject(names.froglogicAddressbookEmailEditText), email); setText(waitForObject(names.froglogicAddressbookPhoneEditText), phone); clickButton(waitForObject(names.froglogicAddressbookSaveButton)); // save userData for last step 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::froglogic_addressbook_addbutton_button)); setText(waitForObject($Names::froglogic_addressbook_forenameedit_text), $forename); setText(waitForObject($Names::froglogic_addressbook_surnameedit_text), $surname); setText(waitForObject($Names::froglogic_addressbook_emailedit_text), $email); setText(waitForObject($Names::froglogic_addressbook_phoneedit_text), $phone); clickButton(waitForObject($Names::froglogic_addressbook_save_button)); # save userData for last step $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::Froglogic_Addressbook_addButton_button)) setText(waitForObject(Names::Froglogic_Addressbook_forenameEdit_text), forename) setText(waitForObject(Names::Froglogic_Addressbook_surnameEdit_text), surname) setText(waitForObject(Names::Froglogic_Addressbook_emailEdit_text), email) setText(waitForObject(Names::Froglogic_Addressbook_phoneEdit_text), phone) clickButton(waitForObject(Names::Froglogic_Addressbook_Save_button)) # save userData for last step 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::froglogic_Addressbook_addButton_button] invoke setText [waitForObject $names::froglogic_Addressbook_forenameEdit_text] $forename invoke setText [waitForObject $names::froglogic_Addressbook_surnameEdit_text] $surname invoke setText [waitForObject $names::froglogic_Addressbook_emailEdit_text] $email invoke setText [waitForObject $names::froglogic_Addressbook_phoneEdit_text] $phone invoke clickButton [waitForObject $names::froglogic_Addressbook_Save_button] # save userData for last step $context userData [dict create forename $forename surname $surname] }
在给定Feature
的所有Scenario
的所有步骤和Hooks
中都可以访问存储在context.userData中的所有数据。最后,我们需要实现步骤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("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD2").innerText, context.userData['forename']) test.compare(waitForObjectExists("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD3").innerText, context.userData['surname'])
Then("previously entered forename and surname shall be at the top", function(context) { test.compare(waitForObjectExists("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD2").innerText, context.userData["forename"], "forename?"); test.compare(waitForObjectExists("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD3").innerText, context.userData["surname"], "surname?"); });
Then("previously entered forename and surname shall be at the top", sub { my $context = shift; test::compare( waitForObject("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD2")->innerText, $context->{userData}{'forename'}, "forename?" ); test::compare( waitForObject("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD3")->innerText, $context->{userData}{'surname'}, "surname?" ); });
Then("previously entered forename and surname shall be at the top") do |context| Test.compare(waitForObjectExists("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD2").innerText, context.userData[:forename], "forename?") Test.compare(waitForObjectExists("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD3").innerText, context.userData[:surname], "surname?") end
Then "previously entered forename and surname shall be at the top" {context} { test compare [property get [waitForObjectExists "DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD2"] innerText] [dict get [$context userData] forename] test compare [property get [waitForObjectExists "DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1.TBODY1.TR2.TD3"] innerText] [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
。
测试执行
在Feature
中的所有Scenario
,或仅执行所选的Scenario
。为了执行所有Scenario
,必须通过在测试套件视图中点击Play
按钮来执行适当的测试用例。
从功能中执行所有场景
为了仅执行一个场景
,您需要打开功能
文件,在指定的场景
上右键单击,然后选择运行场景。另一种方法是点击测试案例资源中场景标签页中相应的场景
旁边的播放按钮。
从功能文件执行一个场景
执行场景
后,功能
文件会根据执行结果进行着色。更详细的信息(如日志)可以在测试结果视图中找到。
功能文件中的执行结果
测试调试
Squish提供在任何位置暂停测试用例执行的可能性,以便检查脚本变量、监视应用程序对象或在Squish脚本控制台中运行自定义代码。为此,必须在启动执行前放置一个断点,无论是在任何包含步骤的功能
文件的任何行,还是在执行的任何代码的任何行(即在步骤定义代码的中间)。
功能文件中的断点
达到断点后,您可以检查所有应用程序对象及其属性。如果在步骤定义或钩子达到时放置断点,则您还可以添加验证点或记录代码片段。
重复使用步骤定义
通过在其他目录中的测试用例中重复使用步骤定义,可以提高BDD测试的可维护性。有关更多信息,请参阅collectStepDefinitions()。
教程:将现有测试迁移到BDD
本章面向有基于脚本的现有测试且希望引入行为测试驱动(BDD)的用户。第一部分描述了如何保留现有测试并以BDD方法简单添加新测试。第二部分描述了如何将现有测试转换为BDD。
扩展现有测试以BDD
创建新的BDD测试用例
假设您的现有测试用例使用了一个库,并且您正在调用共享函数与自动化测试对象(AUT)交互,这些函数仍然可以从BDD测试用例中使用。以下示例中,这些函数被多个基于脚本的测试用例使用。
def numberOfRows(): table = waitForObject(names.dOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) results = table.evaluateXPath(".//TR[contains(@class, 'jqgrow')]") return results.snapshotLength def createNewAddressBook(): clickButton(waitForObject(names.froglogic_Addressbook_newButton_button)) closeConfirm(names.confirmPopup, True)
function numberOfRows() { var table = waitForObject("DOCUMENT.HTML1.BODY1.DIV1.DIV2.DIV3.DIV3.DIV1.TABLE1"); var results = table.evaluateXPath(".//TR[contains(@class, 'jqgrow')]"); return results.snapshotLength; } function createNewAddressBook(){ clickButton(waitForObject(names.froglogicAddressbookNewButtonButton)); closeConfirm(names.confirmPopup, true); }
sub numberOfRows { my $table = waitForObject($Names::document_html1_body1_div1_div2_div3_div3_div1_table1); my $results = $table->evaluateXPath(".//TR[contains(\@class, 'jqgrow')]"); return $results->snapshotLength; } sub createNewAddressBook{ clickButton(waitForObject($Names::froglogic_addressbook_newbutton_button)); closeConfirm($Names::confirmpopup, 1); }
def numberOfRows table = waitForObject(Names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1) results = table.evaluateXPath(".//TR[contains(@class,'jqgrow')]") results.snapshotLength end def createNewAddressBook clickButton(waitForObject(Names::Froglogic_Addressbook_newButton_button)) closeConfirm(Names::ConfirmPopup, true) end
proc numberOfRows {} { set table [waitForObject \ $names::DOCUMENT_HTML1_BODY1_DIV1_DIV2_DIV3_DIV3_DIV1_TABLE1] set results [invoke $table evaluateXPath {.//TR[contains(@class,'jqgrow')]}] return [property get $results snapshotLength] } proc createNewAddressBook {} { invoke clickButton [waitForObject $names::froglogic_Addressbook_newButton_button] invoke closeConfirm $names::ConfirmPopup true }
BDD测试用例可以轻松使用相同的函数
@When("I create a new addressbook") def step(context): createNewAddressBook() test.verify(numberOfRows() == 0)
When("I create a new addressbook", function(context){ createNewAddressBook() test.verify(numberOfRows() == 0); });
When("I create a new addressbook", sub { createNewAddressBook(); test::verify(numberOfRows() == 0); });
When("I create a new addressbook") do |context| createNewAddressBook Test.verify(numberOfRows == 0) end
When "I create a new addressbook" {context} { createNewAddressBook test compare [numberOfRows] 0 }
将现有测试转换为BDD
第二个选项是将现有测试套件
中的基于脚本的测试用例转换为行为驱动测试。由于一个测试套件
可以同时包含基于脚本的和BDD测试用例,因此迁移可以逐步进行。包含这两种测试用例类型的混用测试套件
可以执行并分析结果,而无需做额外的工作。
第一步是审查现有测试套件
中的所有测试用例
,并根据它们所测试的特性
进行分组。每个基于脚本的测试用例将被转换为一个场景
,这是特性
的一部分。例如,假设我们有5个基于脚本的测试用例。审查后,我们认识到它们检查了两个特性
。因此,当迁移完成后,我们的测试套件将包含两个BDD测试用例,每个测试用例包含一个特性
。每个特性
将包含多个场景
。在我们的例子中,第一个特性
包含三个场景
,第二个特性
包含两个场景
。
转换图表
开始时,在squishide
中打开一个包含计划迁移到BDD的Squish测试的测试套件
。接下来,通过选择其上下文菜单中的新建BDD测试用例
选项,创建一个新脚本测试用例
()。每个BDD测试用例包含一个可填充的最大一个特性
的test.feature
文件。接下来,打开test.feature
文件,使用Gherkin语言描述特性
。根据模板的语法,编辑特性
名称,并可选地提供简短说明。接下来,分析将要迁移的测试用例中执行的动作和验证。以下是一个地址簿应用程序示例测试用例的起点:
def main(): startBrowser("http://127.0.0.1:9090/AddressBook.html") confirmPopup(names.newButton_button) test.verify(numberOfRows() == 0, "%d" % numberOfRows())
function main(){ startBrowser("http://localhost:9090/AddressBook.html"); confirmPopup(names.newButtonButton); test.verify(numberOfRows() == 0); }
sub main { startBrowser("http://127.0.0.1:9090/AddressBook.html"); confirmPopup($Names::newbutton_button); test::verify(numberOfRows() == 0); }
def main startBrowser("http://127.0.0.1:9090/AddressBook.html") confirmPopup(Names::NewButton_button) Test.verify(numberOfRows == 0) end
proc main {} { invoke startBrowser "http://127.0.0.1:9090/AddressBook.html" confirmPopup $names::newButton_button test compare [numberOfRows] 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): startBrowser("http://127.0.0.1:9090/AddressBook.html") test.compare(waitForObjectExists("DOCUMENT").title, "froglogic Addressbook") @When("I create a new addressbook") def step(context): confirmPopup(names.newButton_button) @Then("addressbook should have zero entries") def step(context): test.verify(numberOfRows() == 0)
Given("addressbook application is running", function(context) { startBrowser("http://127.0.0.1:9090/AddressBook.html"); test.compare(waitForObjectExists("DOCUMENT").title, "froglogic Addressbook"); }); When("I create a new addressbook", function(context) { confirmPopup(names.newButtonButton); }); Then("addressbook should have zero entries", function(context) { test.verify(numberOfRows() == 0); });
Given("addressbook application is running", sub { my $context = shift; startBrowser("http://127.0.0.1:9090/AddressBook.html"); test::compare(waitForObjectExists("DOCUMENT")->title, "froglogic Addressbook"); }); When("I create a new addressbook", sub { my $context = shift; confirmPopup($Names::newbutton_button); }); Then("addressbook should have zero entries", sub { my $context = shift; test::verify(numberOfRows() == 0); });
Given("addressbook application is running") do |context| startBrowser("http://127.0.0.1:9090/AddressBook.html") Test.compare(waitForObjectExists("DOCUMENT").title, "froglogic Addressbook") end When("I create a new addressbook") do |context| confirmPopup(Names::NewButton_button) end Then("addressbook should have zero entries") do |context| Test.verify(numberOfRows == 0) end
Given "addressbook application is running" {context} { invoke startBrowser "http://127.0.0.1:9090/AddressBook.html" test compare [property get [waitForObjectExists "DOCUMENT"] title] "froglogic Addressbook" } When "I create a new addressbook" {context} { confirmPopup $names::newButton_button } Then "addressbook should have zero entries" {context} { test compare [numberOfRows] 0 }
注意,在将此测试用例迁移到BDD的过程中,删除了test.log("创建新地址簿”)
。当执行步骤我创建一个新的地址簿
时,步骤名称将被记录到测试结果中,因此test.log
调用将是多余的。
此外,当基于脚本的测试用例执行结束时,Squish终止AUT。Squish确保在每个场景
结束时终止AUT。这是通过自动生成的OnScenarioEnd
钩子完成的,如下所示:
@OnScenarioEnd
def hook(context):
closeWindow(":[Window]");
OnScenarioEnd(function(context) {
closeWindow(":[Window]");
});
OnScenarioEnd(sub {
closeWindow(":[Window]");
});
OnScenarioEnd do |context| closeWindow(":[Window]"); end
OnScenarioEnd { context } {
invoke closeWindow ":\[Window\]"
}
上述示例是为了本教程而简化的。为了充分利用Squish中的行为驱动测试,请熟悉行为驱动测试部分,这部分可在API参考中找到。
©2024 The Qt Company Ltd. 包含在本内的文档贡献是各自所有者的版权。
此处提供的文档根据GNU自由文档许可证版本1.3的条款进行许可,该许可证由自由软件基金会发布。
Qt及其相关商标是芬兰及其它国家和地区的The Qt Company Ltd.的商标。所有其他商标均为各自所有者的财产。