JavaFX Squish 教程
学习如何测试JavaFX应用程序。
有关如何测试其他类型Java应用程序的更多信息,请参阅
教程:开始测试JavaFX应用程序
Squish自带IDE和命令行工具。使用squishide
是最简单、最理想的方式,但一旦您构建了大量测试,您会想要自动化它们。例如,进行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或shell脚本中运行的命令行工具是很有价值的。
注意:如果您想获得一些视频指导,Qt Academy提供了45分钟的Squish基础使用在线课程。
我们将测试一个非常简单的地址簿应用程序。用户可以通过对话框添加新地址并删除地址。他们还可以打开和保存地址簿数据文件。尽管该应用程序非常简单,但它包含了你可能希望在自己的测试中使用的所有标准功能,包括菜单、表格和一个具有行编辑和按钮的弹出对话框。一旦你知道如何测试任何这些用户界面元素,你将能够将这些相同的原理应用到你的应用程序中测试的元素,这些元素在教程中没有使用,例如树视图、数值和日期/时间编辑器。有关如何测试列表、表格和树的更全面示例,以及最常见的小部件,包括旋转框,请参阅如何测试应用程序。
截图显示了应用程序正在添加一个新姓名和地址时的动作。
JavaFX addressbook_javafx
示例。
您可以在Squish的示例中找到该应用程序(即,AUT—受测应用程序)。以下章节中将要讨论的测试位于子文件夹中,例如,使用Python编写的测试版本位于SQUISHDIR/examples/java/addressbook_fx/suite_fx
,其他语言的测试以相同命名的子文件夹编写。
注意:在整个手册中,我们经常提到SQUISHDIR
目录。这意味着Squish安装的目录,可能是C:\Squish
、/usr/local/squish
、/opt/local/squish
或其他,这取决于您的安装位置。确切的位置并不重要,只要您在看到本手册中的路径和文件名时将SQUISHDIR
目录心理转换成实际的目录即可。
原则上,测试JavaFX和Java AWT/Swing/SWT应用程序的工作方式相同,所以本教程中描述的所有实践都可以应用到任何一方。唯一的显著区别是,所有这些工具包都使用自己特有的独立的一套小部件和不同的API(应用程序编程接口),因此当我们想与这些小部件交互时(例如,检查特定小部件的属性是否保持特定值时),我们必须当然访问工具包特有的小部件并使用工具包特有的API。
使用示例
第一次尝试运行示例自动测试单元(AUT)的测试时,您可能会遇到一个致命错误,错误信息开始于Squish找不到要启动的AUT...。要从错误中恢复,请点击测试套件设置工具栏按钮,然后在测试应用程序(AUT)部分,如果可用,从下拉框中选择AUT,或点击浏览按钮并在查找对话框中导航到AUT的可执行文件。某些版本的Squish如果未指定AUT将自动打开此对话框。您只需要对每个示例AUT做一次此操作,当测试您自己的AUT时则不需要。
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,并且当所有测试完成后或为每个测试启动和停止时都要停止squishserver。
注意: Squish文档主要使用widget来指代GUI对象,如按钮、菜单、菜单项、标签和表格控件等。Windows用户可能更熟悉控件和容器这两个术语,但在这里我们使用了widget这个词。类似地,macOS用户可能会习惯使用视图这个术语。
使应用程序可测试
在大多数情况下,使应用程序可测试不需要做特殊的事情,因为工具包的API(例如,Qt)提供了足够的功能来实现和记录测试脚本。当squishide
启动AUT时,也会自动建立与squishserver的连接。
创建测试套件
测试套件是测试用例(测试)的集合。使用测试套件很方便,因为它使得在相关测试之间共享测试脚本和测试数据变得容易。
在这里,以及在教程的其余部分,我们首先将描述如何使用squishide
来操作,而命令行用户的说明将在后面。
首先启动squishide
,可以通过单击或双击squishide图标,或者从任务栏菜单启动squishide,或在命令行上执行squishide——您喜欢的方式,并且适合您使用的平台。一旦Squish启动,可能会出现一个欢迎页面,如果您是第一次启动squishide。在右上角单击工作台按钮将其关闭。然后,squishide将看起来类似于截图,但可能略有不同,这取决于您使用的窗口系统、颜色、字体和主题等等。
没有测试套件的squishide
Squish启动后,单击文件 > 新建测试套件将弹出下方的新建测试套件向导。
新建测试套件向导的名称和目录页面
为您的测试套件输入一个名称,并选择您想要存储测试套件的文件夹。在截图中,我们命名为suite_py
,并将它放在addressbook_fx
文件夹中。(对于您的自己的测试,您可能使用更有意义的名称,如"suite_addressbook";我们选择"suite_py"是因为在本教程中,我们将创建几个套件,每个套件对应Squish支持的一种脚本语言。)当然,您可以选择您喜欢的任何名称和文件夹。一旦详细情况完整,单击下一步继续到工具包(或脚本语言)页面。
新建测试套件向导的工具包页面
如果您得到这个向导页面,请点击AUT使用的工具包。在此示例中,我们必须点击Java,因为我们正在测试Java应用程序——AUT是基于JavaFX、AWT/Swing/SWT的,Java选项涵盖了它们所有。然后点击下一步转到脚本语言页面。
新建测试套件向导的脚本语言页面
选择您想要的任何脚本语言——唯一的约束是您只能在每个测试套件中使用一种脚本语言。(所以如果您想使用多种脚本语言,只需创建多个测试套件,每个套件对应您想要使用的脚本语言。)Squish提供的功能对所有语言都是相同的。选择了一种脚本语言后,再次单击下一步进入向导的最后一页。
新建测试套件向导的AUT页面
如果您正在为一个Squish已经认识的AUT创建一个新的测试套件,只需单击组合框以显示AUT列表并选择所需的AUT。如果组合框为空或您的AUT未列出,请单击组合框右侧的浏览按钮并导航到您的AUT: SQUISHDIR/examples/java/addressbook_fx/AddressBook.jar
。一旦选择,请单击完成,Squish将在具有与测试套件相同名称的子文件夹中创建一个文件夹,并在该文件夹内创建一个名为suite.conf
的文件,其中包含测试套件的配置细节。Squish还将AUT注册到squishserver。此时,向导将关闭,squishide
将类似于下面的截图。
squishide
与suite_py测试套件
我们现在可以开始创建测试。请继续阅读以了解如何在无需使用squishide
的情况下创建测试套件,或者跳转到录制测试和验证点。
从命令行创建测试套件
要从命令行创建新的测试套件
- 创建一个新目录以保存测试套件——目录名应以前缀
suite
开始。在此示例中,我们已创建了SQUISHDIR/examples/java/addressbook_fx/suite_py
目录以存储Python测试。(我们也有其他语言的类似子目录,但这纯粹是为了示例,因为我们通常只使用一种语言进行所有测试。) - 将AUT注册到squishserver。
注意:每个AUT都必须与squishserver注册,这样测试脚本就不需要包含AUT的路径,从而使测试具有平台无关性。注册的另一个好处是可以在不使用
squishide
的情况下测试AUT——例如,在执行回归测试时。这是通过在命令行上使用squishserver执行带有
--config
选项和addAUT
命令完成的。例如,假设我们位于Linux上的squish
目录中squishserver --config addAUT AddressBook.jar \ SQUISHDIR/examples/java/addressbook_fx
Windows用户将使用\代替/作为其目录分隔符。
我们必须为
addAUT
命令提供AUT的可执行文件名以及单独的AUT路径。在这种情况下,路径是指测试套件配置文件中添加为AUT的.class
文件。有关应用程序路径的更多信息,请参阅AUTs和设置。 - 在套件子目录中创建一个名为
suite.conf
的纯文本文件(ASCII或UTF-8编码)。这是测试套件的配置文件,并且至少必须识别AUT、用于测试的脚本语言以及AUT使用的包装器(即,GUI工具包或库)。该文件的格式是key
=
value
,每行一个键-值对。例如AUT = AddressBook.jar LANGUAGE = Python WRAPPERS = Java OBJECTMAPSTYLE = script
AUT的名称是您在上一个步骤中注册的内容。 Lang可以设置为您喜欢的任何语言——目前Squish能够支持JavaScript、Python、Perl、Ruby和Tcl,但确切的可用性可能因Squish的安装方式而异。对于JavaFX和AWT/Swing/SWT程序,设置Wrappers为
Java
就足够了。
录制测试和验证点
Squish使用指定给测试套件的脚本语言录制测试。一旦录制了测试,我们就可以运行该测试,Squish将忠实地重复我们在录制测试时所执行的所有操作,但不会像人类那样出现暂停。此外,还可以编辑记录的测试,或者将记录的测试的一部分复制到手动创建的测试中,正如我们稍后在教程中将要看到的那样。
录音将记录到现有测试用例中。您可以通过以下方式创建一个新脚本文本测试用例
- 选择文件 > 新测试用例来打开新Squish测试用例向导,输入测试用例名称,然后选择完成。
- 在测试套件视图中,点击测试用例标签右侧的新脚本文本测试用例()工具栏按钮。这将创建一个具有默认名称的新测试用例,您可以轻松将其更改。
为新测试用例命名为“tst_general”。
Squish会自动在测试套件文件夹内创建一个名为此名称的子文件夹以及一个测试文件,例如test.py
。如果您选择JavaScript作为脚本语言,则文件命名为test.js
,对于Perl、Ruby或Tcl也是如此。
使用tst_general测试用例的squishide
如果您收到一个.feature文件的样本而不是“Hello World”脚本,点击运行测试套件()旁边的箭头,然后选择新脚本文本测试用例()。
为了使测试脚本文件(如test.js
或test.py
)出现在一个编辑器视图中,请单击或双击测试用例,具体取决于首选项 > 常规 > 打开模式设置。这选择脚本作为活动脚本,并使其对应的录制()和运行测试()按钮可见。
复选框用于控制当点击运行测试套件()工具栏按钮时运行哪些测试用例。我们还可以通过点击单个测试用例的运行测试()按钮来运行单个测试用例。如果测试用例不是当前活动状态,按钮可能在鼠标悬停之前不可见。
最初,脚本的main()
将Hello World记录到测试结果中。为了手动创建测试,就像我们在以下教程中将要做的那样,我们必须创建一个main
函数,并且应在顶部导入相同的导入。在Squish中,main
是一个特殊的名称。您可以包含与脚本语言支持一样多的函数和其他代码,当测试执行时(即运行时),Squish始终执行main
函数。您可以像在如何创建和使用共享数据和共享脚本中描述的那样,在测试脚本之间共享常用代码。
还有两个其他函数名称也是Squish的特殊名称:cleanup
和init
。有关更多信息,请参阅创建特殊函数。
创建新测试用例后,我们可以自由地手动编写测试代码或录制测试。通过单击测试用例的录制()按钮来替换测试代码的新录制。或者,您还可以根据如何编辑和调试测试脚本中的说明录制片段并将其插入现有测试用例。
从命令行创建测试
要从命令行创建新测试用例
- 在测试套件目录中创建一个新子目录。例如,在
SQUISHDIR/examples/java/addressbook_fx/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"。不用担心输入错误——只需按退格删除即可正常修正并正确输入。最后,单击《b translate="no">确定 按钮。现在应该出现一个新地址,包含您键入的详细信息。
- 现在单击第一行,然后单击 编辑 > 删除,然后在消息框中单击 是 按钮。第一行应该已删除,因此您的 "Jane Doe" 项目现在是第一个。
- 单击 Squish 控制栏中的 验证 工具栏按钮,并在下拉列表中选择 属性。这将使
squishide
出现。 - 在 应用程序对象 视图中,展开
Stage
对象,然后是Scene
、Root
和TableView
对象。现在展开TableRow0
对象。点击前两个子对象,使它们的属性在 属性视图 中出现。对于每一个,检查text
属性。最后,单击 (按钮 (在验证点创建器视图) 底部的 保存并插入验证 按钮,将验证点插入 recordings test 脚本。 (参照片)。一旦插入验证点,squishide
窗口将再次隐藏,并且控制栏窗口和 AUT 将回到可见状态。显示即将插入的两个验证点的
squishide
- 我们现在已完成了测试,所以点击 AUT 中的《b translate="no">文件 > 退出,然后在消息框中点击《b translate="no">不,因为我们不希望保存任何更改。
一旦我们退出 AUT,录制的测试将如同照片所示出现在 squishide
中。 (注意,实际记录的代码会根据您的交互方式而变化。例如,您可以单击或使用按键序列来调用菜单选项——使用哪种方式不重要,但它们是不同的,因此 Squish 将不同地记录它们。)
显示录制了“tst_general”测试的《code translate="no">squishide
如果录制的测试没有出现,调用(或者根据您的平台和设置单击或双击)《code translate="no">tst_general 测试用例,这将使 Squish 在编辑器窗口中显示测试的 test.py
文件,如照片所示。
JavaFX特定的chooseFile(objectOrName, filename)函数将一个文件名作为用户与标准窗口系统提供的文件打开对话框交互一样处理。Squish记录文件的绝对路径以确保测试运行时的可靠性,但在这个教程中,我们使用了脚本语言功能来提供在任何示例安装处都能工作的文件名。
现在我们已经记录了测试,我们能够回放它,即运行它。本身很有用,如果在回放过程中失败,这可能意味着应用程序已经出错了。此外,截图显示的我们所添加的两个验证将在回放时进行检查。
在测试录制期间插入验证点非常方便。然而,有时我们可能忘记插入验证,或者以后我们可能希望添加新的验证。就像下一段将展示的那样,我们可以轻松地向录制的测试脚本中添加额外的验证。
在继续之前,我们将探讨如何从命令行录制测试。然后我们将看到如何运行测试,并且我们还将查看一些Squish录制的测试所生成的代码,并讨论其一些特性。
从命令行录制测试
在录制测试时,始终需要运行squishserver。这由squishide自动处理,但命令行用户必须手动启动squishserver,如squishserver中所述。
要从命令行录制测试,请执行squishrunner程序并指定要记录的测试套件以及测试用例的名称。例如,假设我们处于包含测试套件目录的目录中
squishrunner --testsuite suite_py --record tst_general --useWaitFor
始终最好使用带有--useWaitFor选项进行录制,该选项记录对Object waitForObject(objectOrName)函数的调用,尽管它是默认值,是出于历史原因,但它的可靠性比使用snooze(seconds)函数要高。squishide自动使用Object waitForObject(objectOrName)函数。
当连接了多个设备和/或模拟器时,需要使用--device some-device
来指定目标。
从IDE中运行测试
在squishide中运行一个测试用例时,当测试用例位于测试套件视图中鼠标悬停或被选中时,将显示运行测试()按钮。
要一个接一个地运行两个或多个测试用例,或者只运行选定的测试用例,请单击运行测试套件()。
从命令行运行测试
运行测试时,始终需要运行squishserver,或者必须向squishrunner提供--local
选项。有关更多信息,请参阅squishserver。
要从命令行回放录制的测试,请执行squishrunner程序,指定录制的脚本所在的测试套件以及我们希望回放的测试用例。例如,假设我们处于包含测试套件目录的目录中
squishrunner --testsuite suite_py --testcase tst_general --local
在某些情况下,运行测试可能会失败并显示出类似于以下的消息
ERROR Mon Mar 23 15:41:12 2009 test.py:31: Script Error Error in closeWindow() invocation: null object
有时,如果对话框被 dispose()
操作关闭得太快,就可能会发生这种情况。只需转到出问题的行,注释掉 closeWindow()
调用(在本例中为第18行)并重新运行测试。现在测试应该可以正常工作了。
分析生成的代码
如果您查看截图中的代码(或下面的代码片段),您会发现它包含许多 Object waitForObject(objectOrName) 调用,这些调用作为各种其他调用的参数,例如 activateItem(objectOrName, itemText)、clickButton(objectOrName)、mouseClick(objectOrName) 和 type(objectOrName, text)。Object waitForObject(objectOrName) 函数等待直到 GUI 对象准备好交互(即变为可见和启用),然后跟随一些与之交互的函数。典型交互是激活(弹出)菜单、点击菜单选项或按钮,或输入一些文本。
要了解 Squish 脚本命令的完整概述,请参阅 如何创建测试脚本、如何测试应用程序 - 具体内容、API 参考文档 和 工具参考文档。
对象通过 Squish 生成的名称进行标识。有关详细信息,请参阅 如何识别和访问对象。
生成的代码大约有 30 行。以下是一个摘录,仅显示了 Squish 记录编辑菜单的“添加”选项、在“添加”对话框中输入 Jane Doe 的详细信息并最终点击“确定”关闭对话框并更新表的步骤。
注意:虽然截图只显示了 Python 测试套件的运行情况,但在此处引用以及整个教程中引用的代码片段中,我们展示了 Squish 支持的所有脚本语言的代码。当然,在实际应用中,您通常只会使用其中的一种,因此请自由地查看您感兴趣的代码片段,跳过其他代码。
mouseClick(waitForObject(names.editAddButton_Button), 21, 22, 0, Button.Button1) type(waitForObject(names.address_Book_Add_forenameText_TextField), "Jane") type(waitForObject(names.address_Book_Add_forenameText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_surnameText_TextField), "Doe") type(waitForObject(names.address_Book_Add_surnameText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_emailText_TextField), "[email protected]") type(waitForObject(names.address_Book_Add_emailText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_phoneText_TextField), "123 555 1212") mouseClick(waitForObject(names.address_Book_Add_OK_Button), 35, 7, 0, Button.Button1)
mouseClick(waitForObject(names.editAddButtonButton), 21, 17, 0, Button.Button1); type(waitForObject(names.addressBookAddForenameTextTextField), "Jane"); type(waitForObject(names.addressBookAddForenameTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddSurnameTextTextField), "Doe"); type(waitForObject(names.addressBookAddSurnameTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddEmailTextTextField), "[email protected]"); type(waitForObject(names.addressBookAddEmailTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddPhoneTextTextField), "123 555 1212"); mouseClick(waitForObject(names.addressBookAddOKButton), 50, 5, 0, Button.Button1);
mouseClick(waitForObject($Names::editaddbutton_button), 14, 22, 0, Button->Button1); type(waitForObject($Names::address_book_add_forenametext_textfield), "Jane"); type(waitForObject($Names::address_book_add_forenametext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_surnametext_textfield), "Doe"); type(waitForObject($Names::address_book_add_surnametext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_emailtext_textfield), "Jane.doe\@nowhere.com"); type(waitForObject($Names::address_book_add_emailtext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_phonetext_textfield), "123 555 1212"); mouseClick(waitForObject($Names::address_book_add_ok_button), 36, 14, 0, Button->Button1);
mouseClick(waitForObject(Names::EditAddButton_button), 19, 16, 0, Button::BUTTON1) type(waitForObject(Names::Address_Book_Add_forenameText_TextField), "Jane") type(waitForObject(Names::Address_Book_Add_forenameText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_surnameText_TextField), "Doe") type(waitForObject(Names::Address_Book_Add_surnameText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_emailText_TextField), "[email protected]") type(waitForObject(Names::Address_Book_Add_emailText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_phoneText_TextField), "123 555 1212") mouseClick(waitForObject(Names::Address_Book_Add_OK_Button), 35, 15, 0, Button::BUTTON1)
invoke mouseClick [waitForObject $names::editAddButton_button] 15 18 0 [enum Button Button1] invoke type [waitForObject $names::Address_Book_Add_forenameText_TextField] "Jane" invoke type [waitForObject $names::Address_Book_Add_forenameText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_surnameText_TextField] "Doe" invoke type [waitForObject $names::Address_Book_Add_surnameText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_emailText_TextField] "[email protected]" invoke type [waitForObject $names::Address_Book_Add_emailText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_phoneText_TextField] "123 555 1212" invoke mouseClick [waitForObject $names::Address_Book_Add_OK_Button] 32 11 0 [enum Button Button1]
如你所见,测试人员使用了键盘在文本字段之间进行标签切换,并用鼠标点击 OK。如果测试人员通过点击鼠标移动焦点,并通过按空格键或任何其他交互组合将鼠标光标移至 OK 按钮并按空格键,结果将是相同的,但当然 Squish 会记录实际采取的操作。
请注意,代码片段中没有显式延迟。(可以使用 Squish 的 snooze(seconds) 函数强制延迟。)这是因为 Object waitForObject(objectOrName) 函数会延迟,直到给它提供的对象准备好——这样 Squish 就可以尽可能快地运行,但不会更快。
Squish 记录在引用对象时使用以 names.
前缀开始的变量,这标识了它们为 象征性名称。每个变量都包含作为值的相应的 实际名称,这可以是基于字符串的,也可以实现为属性值到值的键值映射。Squish 支持多种命名方案,并且在脚本中都可以使用并混合。使用象征性名称的优点是,如果应用程序发生更改,需要不同的名称,我们可以简单地更新 Squish 的对象映射(将象征性名称与实际名称相关联),从而避免需要更改我们的测试脚本。有关对象映射的更多信息,请参阅 对象映射 和 对象映射视图。
当光标位于符号名称下时,编辑器的上下文菜单允许您打开符号名称,显示其在中国地图中的条目,或转换为真实名称,在脚本语言中放置一个内联映射,以允许您在脚本本身中手动编辑属性。
既然我们已经看到如何记录和回放测试,也看到了Squish生成的代码,那么我们再进一步,确保在测试执行过程中的特定点上某些条件成立。
插入附加的验证点
在前一个部分中,我们看到了在记录测试脚本期间如何轻松地插入验证点。验证点也可以被插入到现有的测试脚本中,无论是通过设置断点并使用squishide
,还是简单地通过编辑测试脚本并调用Squish的测试函数(如布尔 test.compare(value1, value2)和布尔 test.verify(condition))。
Squish支持许多类型的验证点:验证对象属性具有特定值的验证(称为“对象属性验证”);验证整个表包含我们所期望的内容(称为“表格验证”);验证两个图像匹配(称为“截图验证”;以及一种混合验证类型,它包含来自多个对象的属性和截图,称为“视觉验证”。此外,还可以验证搜索图像是否存在于屏幕上的某个位置,或者通过OCR找到某些文本。最常用的类型是对象属性验证,这就是我们在教程中要介绍的内容。有关更多信息,请参阅如何创建和使用验证点)。
(非脚本化的)属性验证点以XML文件的形式存储在测试用例或测试套件资源中,包含需要传递给test.compare()
的值。这些验证点可以在多个测试用例之间重用,并可以在单一行的脚本代码中验证许多值。
(脚本化的)属性验证点是直接调用布尔 test.compare(value1, value2)函数,包含两个参数——特定对象特定属性的价值和一个期望的价值。我们可以在记录或手工编写的脚本中手动插入布尔 test.compare(value1, value2)函数的调用,或者我们可以让Squish帮助我们使用脚本化验证点插入它们。在前一个部分中,我们展示了如何使用squishide
在记录期间插入验证。这里我们将首先展示如何使用squishide
将验证插入到现有的测试脚本中,然后我们将展示如何手动插入验证。
在请求Squish插入验证点之前,最好确保我们有一个要验证的内容和时间的列表。我们可以有许多可以添加到tst_general
测试用例中的潜在验证,但鉴于我们在这里的关注只是展示如何做,我们只会做两件事——我们将验证“Jane Doe”条目的电子邮件地址和电话号码是否与输入的匹配,并将验证直接放在我们记录期间插入的验证之后。
要使用squishide
插入验证点,我们首先在想要验证的脚本中放置一个断点(无论是记录的还是手动编写的——对Squish来说并不重要)。
显示带有断点的squishide
的tst_general测试用例
如图示所示,我们在第24行设置了断点。只需双击或右键点击编辑器旁边的空白处(行号旁边)并选择添加断点上下文菜单项即可完成。选择该行的原因是它紧随删除第一个地址的脚本行之后,因此在此处(在调用文件菜单来关闭应用程序之前),第一个地址应该是“Jane Doe”。截图显示的是使用squishide
在录制过程中输入的验证。我们的额外验证将遵循这些。(注意,如果您以不同的方式录制了测试,例如使用键盘快捷键而不是菜单项,则您的行号可能不同。)
设置了断点后,我们现在可以通过单击运行测试()或单击运行 > 运行测试用例菜单选项来像往常一样运行测试。与正常的测试运行不同,当达到断点时,测试将停止,Squish的主窗口将重新出现(可能会遮挡AUT)。此时,squishide
将自动切换到测试调试视角。
视角和视图
squishide
的功能与Eclipse IDE相似。如果您不熟悉Eclipse,理解以下关键概念是至关重要的:视图 和 视角。在Eclipse中,因此也在squishide
中,一个视图基本上是一个子窗口,例如停靠窗口或现有窗口中的选项卡。一个视角是一系列视图的组合。这两个都可以通过窗口菜单访问。
squishide
提供了以下视角
您可以更改这些视角以显示额外的视图或隐藏不想要的视图,或者创建包含您想要的视图的自己的视角。所以如果您的窗口发生了显著变化,这就意味着视角已更改。使用窗口菜单来切换回您想要的视角。但是,Squish会自动更改视角以反映当前情况,因此您通常不需要手动更改视角。
插入验证点
如下面的截图所示,当Squish在断点处停止时,squishide
会自动切换到测试调试视角。该视角显示了变量视图、编辑器视图、调试视图、应用程序对象视图和属性视图、方法视图和测试结果视图。
要插入一个验证点,我们可以在应用程序对象视图中展开项目,直到找到要验证的对象,或者我们可以使用对象选择器()。在这个例子中,我们想验证TableView
的第一行文本,所以我们可以展开根
项目及其子项目,直到找到TableView
,然后在该TableRow
中找到正确的TableCellProxy
,它包含我们感兴趣的数据。一旦我们点击TableCellProxy对象,它的属性就像截图显示的那样显示在属性视图中。
在应用程序对象视图中选择要验证的对象
在任何时候,通过从窗口菜单中选择它(或单击其工具栏按钮),可以返回正常的测试管理视角,尽管如果您停止脚本或运行到完成,squishide
将自动返回到它。
在这里,我们可以看到行0和列0中代理的文本
属性值为"Jane";我们在录制过程中已经为此添加了验证。向下滚动以便可以看到该行中的电子邮件条目。要确保每次运行测试时都进行验证,请点击应用程序对象视图中的代理项以显示其属性,然后单击文本
属性以检查其复选框。当我们检查它时,就像截图显示的那样,将出现验证点创建视图。
选择要验证的属性值
此时,验证点尚未添加到测试脚本中。我们可以通过单击保存并插入验证按钮轻松添加它。但在做这件事之前,我们将添加另一个要验证的项。
向下滚动并单击应用程序对象视图中的电话号码代理;然后检查其文本
属性。现在,两个验证都将显示在验证点创建视图中,如图所示。
选择多个属性值进行验证
我们现在已经说过,我们预计这些属性值将与我们显示的值相同,即电子邮件地址为"[email protected]"和电话号码为"555 123 4567"。我们必须点击保存并插入验证按钮才能实际上插入验证点,所以请现在这样做。
我们不需要继续运行测试,所以我们可以在这个点上停止运行测试(通过单击停止工具栏按钮),或者我们可以继续(通过单击恢复按钮)。
一旦我们完成插入验证并停止或完成测试的运行,我们现在应该禁用断点。只需右键单击断点,在上下文菜单中单击禁用断点菜单选项。我们现在可以运行测试,没有断点但有验证点。单击运行测试()。这一次,我们将得到一些额外的测试结果——如图所示,我们将其展开以显示其详细信息。(我们还将选择Squish插入以执行验证的代码行——请注意,代码结构与录制期间插入的代码相同。)
新插入的验证点正在执行中
手动插入验证点的一种方法是手动编写。我们可以在现有脚本中添加自己对 Squish 测试函数的调用,例如 布尔值 test.compare(value1, value2) 和 布尔值 test.verify(condition)。实际上,最好首先确保 Squish 了解我们想要验证的对象,以便测试运行时能找到它们。首先,我们在打算添加验证的地方设置断点。然后,我们运行测试脚本直到它停止。接下来,我们使用 对象选择器 ( ) 或在 应用程序对象视图 中导航,直到找到我们想要验证的对象。这时,右键单击我们感兴趣的对象并点击 添加到对象映射 快捷菜单选项是非常明智的。这将确保 Squish 可以访问对象。然后再次右键单击并点击 复制符号名 快捷菜单选项——这将给我们 Squish 将用来识别对象的名称。现在我们可以编辑测试脚本,加入自己的验证并完成或停止执行。(当断点不再需要时,别忘了禁用断点。)
虽然我们可以编写与我们自动生成的代码完全相同的测试脚本代码风格,但如我们将要解释的,通常采用略微不同的风格会更清晰、更易于执行。
对于我们的手动验证,我们想要检查在读取 MyAddresses.adr
文件后、新地址添加后以及第一个地址删除后,TableView
中存在的地址数量。截图显示了我们所输入的代码中的一行,用以获取这三个验证之一,以及运行测试脚本的结果。
手动输入的验证点在实际应用中
当我们手动编写脚本时,我们使用 Squish 测试模块的函数来比较或验证测试脚本执行过程中的某些点上的条件。截图(以及下面的代码片段)显示,我们首先获取对我们感兴趣的对象的引用。对于手动编写的测试脚本,使用 Object waitForObject(objectOrName) 函数是标准做法。这个函数等待对象可用(即可见和启用),然后返回对该对象的引用。(否则它会超时并抛出一个可捕获的异常。)然后我们使用这个引用来访问属性——在这个例子中是 TableView
's items.length ——然后使用 布尔值 test.compare(value1, value2) 函数来验证该值是否是我们预期的。
以下是我们在所有 Squish 支持的脚本语言中手动输入的第一个验证代码。当然,你只需要查看你将为自己测试所使用的语言的代码。
mouseClick(waitForObjectItem(names.address_Book_MyAddresses_adr_itemTbl_TableView, "1/0"), 70, 13, 0, Button.Button1) mouseClick(waitForObject(names.editAddButton_Button), 21, 22, 0, Button.Button1) type(waitForObject(names.address_Book_Add_forenameText_TextField), "Jane") type(waitForObject(names.address_Book_Add_forenameText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_surnameText_TextField), "Doe") type(waitForObject(names.address_Book_Add_surnameText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_emailText_TextField), "[email protected]") type(waitForObject(names.address_Book_Add_emailText_TextField), "<Tab>") type(waitForObject(names.address_Book_Add_phoneText_TextField), "123 555 1212") mouseClick(waitForObject(names.address_Book_Add_OK_Button), 35, 7, 0, Button.Button1) test.compare(loadedTable.items.length, 126)
mouseClick(waitForObjectItem(names.addressBookMyAddressesAdrItemTblTableView, "6/0"), 92, 21, 0, Button.Button1); mouseClick(waitForObjectItem(names.addressBookMyAddressesAdrItemTblTableView, "2/0"), 87, 11, 0, Button.Button1); mouseClick(waitForObject(names.editAddButtonButton), 21, 17, 0, Button.Button1); type(waitForObject(names.addressBookAddForenameTextTextField), "Jane"); type(waitForObject(names.addressBookAddForenameTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddSurnameTextTextField), "Doe"); type(waitForObject(names.addressBookAddSurnameTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddEmailTextTextField), "[email protected]"); type(waitForObject(names.addressBookAddEmailTextTextField), "<Tab>"); type(waitForObject(names.addressBookAddPhoneTextTextField), "123 555 1212"); mouseClick(waitForObject(names.addressBookAddOKButton), 50, 5, 0, Button.Button1); test.compare(loadedTable.items.length, 126)
mouseClick(waitForObjectItem($Names::address_book_myaddresses_adr_itemtbl_tableview, "1/0"), 71, 8, 0, Button->Button1); mouseClick(waitForObject($Names::editaddbutton_button), 14, 22, 0, Button->Button1); type(waitForObject($Names::address_book_add_forenametext_textfield), "Jane"); type(waitForObject($Names::address_book_add_forenametext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_surnametext_textfield), "Doe"); type(waitForObject($Names::address_book_add_surnametext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_emailtext_textfield), "Jane.doe\@nowhere.com"); type(waitForObject($Names::address_book_add_emailtext_textfield), "<Tab>"); type(waitForObject($Names::address_book_add_phonetext_textfield), "123 555 1212"); mouseClick(waitForObject($Names::address_book_add_ok_button), 36, 14, 0, Button->Button1); test::compare($loadedTable->items->length, 126);
mouseClick(waitForObjectItem(Names::Address_Book_MyAddresses_adr_itemTbl_TableView, "4/1"), 41, 14, 0, Button::BUTTON1) mouseClick(waitForObject(Names::EditAddButton_button), 19, 16, 0, Button::BUTTON1) type(waitForObject(Names::Address_Book_Add_forenameText_TextField), "Jane") type(waitForObject(Names::Address_Book_Add_forenameText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_surnameText_TextField), "Doe") type(waitForObject(Names::Address_Book_Add_surnameText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_emailText_TextField), "[email protected]") type(waitForObject(Names::Address_Book_Add_emailText_TextField), "<Tab>") type(waitForObject(Names::Address_Book_Add_phoneText_TextField), "123 555 1212") mouseClick(waitForObject(Names::Address_Book_Add_OK_Button), 35, 15, 0, Button::BUTTON1) Test.compare(loadedTable.items.length, 126)
invoke mouseClick [waitForObjectItem $names::Address_Book_MyAddresses_adr_itemTbl_TableView "4/0"] 71 6 0 [enum Button Button1] invoke mouseClick [waitForObject $names::editAddButton_button] 15 18 0 [enum Button Button1] invoke type [waitForObject $names::Address_Book_Add_forenameText_TextField] "Jane" invoke type [waitForObject $names::Address_Book_Add_forenameText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_surnameText_TextField] "Doe" invoke type [waitForObject $names::Address_Book_Add_surnameText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_emailText_TextField] "[email protected]" invoke type [waitForObject $names::Address_Book_Add_emailText_TextField] "<Tab>" invoke type [waitForObject $names::Address_Book_Add_phoneText_TextField] "123 555 1212" invoke mouseClick [waitForObject $names::Address_Book_Add_OK_Button] 32 11 0 [enum Button Button1] test compare [property get [waitForObjectExists $names::itemTbl_Jane_TableCell] text] "Jane"
编码模式非常简单:我们获取对我们感兴趣的对象的引用,然后使用 Squish 的验证函数之一验证其属性。而且,如果我们愿意,当然可以调用对象上的方法与其互动。
有关手动编写代码的更多示例,请参阅 手动创建测试、如何创建测试脚本 和 如何测试应用程序 - 特定细节。
有关验证点的全面覆盖,请参阅 如何创建和使用验证点。
测试结果
每次测试运行结束后,包括验证点在内的测试结果都会在 squishide
底部的 测试结果 视图中显示。
这是一份详尽的测试运行报告,其中还会包含任何故障或错误的详细信息等。如果您点击一个测试结果项,squishide
会将生成测试结果的脚本行高亮显示。如果您展开一个测试结果项,您可以看到测试的更多详细信息。
Squish的测试结果界面非常灵活。通过实现自定义报表生成器,可以以许多不同的方式处理测试结果,例如将其存储在数据库中,或者输出为HTML文件。默认报表生成器在通过命令行运行Squish时,将结果简单地打印到stdout
,或在使用squishide
时输出到测试结果视图。您可以通过在测试结果上右键单击并选择导出结果菜单选项,将测试结果从squishide
保存为XML。有关其他报表生成器的列表(不可从此处访问),请参阅squishrunner –reportgen:生成报表。还可以直接将测试结果记录到数据库中。有关详细信息,请参阅如何从Squish测试脚本访问数据库。
如果您使用squishrunner在命令行上运行测试,您还可以以不同的格式导出结果并将其保存到文件中。有关更多信息,请参阅处理测试结果和{如何使用测试语句}部分。
手动创建测试
现在我们已经了解了如何通过插入验证点来记录和修改测试,我们可以了解如何手动创建测试。最简单的方法是修改和重构已记录的测试,尽管从头开始创建手动测试也是完全可以的。
编写手动测试最具挑战性的部分可能是使用正确的对象名称,但在实践中,这很少成为问题。我们可以复制Squish在记录上一个测试时已经添加到对象映射的符号名称,或者我们可以直接从已记录的测试中复制对象名称。如果我们还没有记录任何测试且是从零开始的,我们可以使用间谍程序。我们通过单击工具栏上的启动AUT按钮来完成此操作。这会启动AUT并将其切换到间谍视角。然后我们可以与AUT交互,直到我们感兴趣的对象可见为止。然后,在squishide
中,我们可以导航到或选择该对象,使其在应用程序对象视图中选择,并使用上下文菜单来添加到对象映射和复制(符号名称 | 实际名称)到剪贴板(这样我们就可以将其粘贴到我们的测试脚本中)。最后,我们可以单击工具栏上的退出AUT按钮以终止AUT并返回到测试管理视角。有关使用间谍的更多详细信息,请参阅如何使用间谍。
我们可以通过点击工具栏上的对象图按钮(在测试用例窗口中)或从脚本编辑器的上下文菜单中,当在脚本的名称上右击时选择打开符号名称来查看对象图(另见,对象图)。Squish与之交互的每个应用程序对象都列在这里,无论是顶级对象还是子对象(视图是一个树形视图)。我们可以通过右击我们要关心的对象,然后在上下文菜单中选择复制对象名称(以获取符号名称变量)或复制实名(以获取存储在变量中的实际键值对)来检索Squish在录制脚本中使用的符号名称。这在我们要修改现有的测试脚本或创建从零开始的测试脚本时很有用,就像我们将在教程中稍后看到的那样。
修改和重构已录制的测试
假设我们想要通过添加三个新名称和地址来测试AUT的添加功能。当然,我们可以录制这样的测试,但通过代码来完成一切也同样容易。测试脚本需要执行的步骤是:首先点击文件 > 新建创建一个新的地址簿,然后对于每个新名称和地址,点击编辑 > 添加,填写详细信息,然后点击确定。最后,不保存任何内容就点击文件 > 退出。我们还希望在开始时验证没有数据行,并在结束时有三行。我们将边做边重构,以使代码尽可能整洁和模块化。
首先我们必须创建一个新测试用例。点击文件 > 新建测试用例并将测试用例的名称设置为tst_adding
。Squish将自动创建一个test.py
(或test.js
等)文件。
命令行用户可以在测试用例目录中创建一个名为tst_adding
的新目录,并在该目录内创建和编辑test.py
文件(或test.js
等)。
我们需要的第一件事是启动AUT并调用一个菜单选项的方法。以下是录制tst_general
脚本的最初几行:
import names def main(): startApplication("AddressBook.jar") activateItem(waitForObjectItem(names.address_Book_MenuBar, "_File")) activateItem(waitForObjectItem(names.address_Book_File_ContextMenu, "_Open..."))
import * as names from 'names.js'; function main() { startApplication("AddressBook.jar"); activateItem(waitForObjectItem(names.addressBookMenuBar, "_File")); activateItem(waitForObjectItem(names.addressBookFileContextMenu, "_Open..."));
require 'names.pl'; sub main { startApplication("AddressBook.jar"); activateItem(waitForObjectItem($Names::address_book_menubar, "_File")); activateItem(waitForObjectItem($Names::address_book_file_contextmenu, "_Open..."));
require 'squish' require 'names' include Squish def main startApplication("AddressBook.jar") activateItem(waitForObjectItem(Names::Address_Book_MenuBar, "_File")) activateItem(waitForObjectItem(Names::Address_Book_File_ContextMenu, "_Open..."))
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "AddressBook.jar" invoke activateItem [waitForObjectItem $names::Address_Book_MenuBar "_File"] invoke activateItem [waitForObjectItem $names::Address_Book_File_ContextMenu "_Open..."]
请注意代码的模式很简单:启动AUT,然后等待菜单栏,然后激活菜单栏;等待菜单项,然后激活菜单项。在这两种情况下,我们都使用了Object waitForObjectItem(objectOrName, itemOrIndex)函数。此函数用于多值对象(如列表、表、树——或者在这种情况下,菜单栏和菜单),并允许我们通过传递包含项的对象的名称和项的文本作为参数来访问对象项(这些项当然是对象)。
注意:将函数放入tst_adding
可能看起来有些浪费,因为我们也可以在tst_general
和其他测试用例中使用它们。然而,为了保持教程简单起见,我们将代码放入tst_adding
测试用例中。有关如何共享脚本的详细信息,请参阅如何创建和使用共享数据和共享脚本。
如果您查看已记录的测试(tst_general
)或检查对象映射,您会发现Squish有时会对相同的事物使用不同的名称。例如,MenuBar有两个不同的标识方式,最初是names.address_Book_menu_bar
,后来是names.address_Book_MyAddresses_adr_menu_bar
。原因是Squish使用标题来标识Stage
(主窗口),每次标题更改都会赋予它一个新的身份。由于许多对象是这个的子项,包括MenuBar,这决定了它的对象名称。
也许,当我们手动编写测试脚本时,我们不关心标题,并希望为所有测试用例使用Stage
及其子项的统一名称。为此,我们可以编辑names.address_Book_Stage
的条目,并从其实际名称中删除标题,使其在更多场合与主窗口匹配。
如果在测试执行期间AUT看起来冻结了,请等待Squish超时AUT(约20秒),并显示对象未找到对话框,指示如下的错误
这通常意味着Squish在对象映射中找不到具有给定名称的对象或属性值。从这里,我们可以选择新的对象,调试,抛出错误,或者在选择新对象后,重试。
选择新对象将更新符号名称的对象映射条目。除了对象选择器()外,我们还可以使用小偷的应用程序对象视图来定位我们感兴趣的对象,并使用将对象添加到对象映射上下文菜单操作来访问它们的实际或符号名称。
命名很重要,因为这可能是编写脚本中最容易出错的部分,通常是上面的对象...未找到错误。一旦我们在测试中确定了要访问的对象,用Squish编写测试脚本就非常直接。特别是,因为Squish很可能支持您最熟悉的脚本语言。
我们现在几乎准备好编写自己的测试脚本了。最好先录制一个模拟测试。所以点击文件 > 新建测试用例,将测试用例的名称设置为tst_dummy
。然后点击模拟测试用例的录制()。一旦AUT启动,点击文件 > 新建,然后点击(空)表格,然后点击编辑 > 添加,添加一个条目,然后按Return或点击确定。最后,点击文件 > 退出以完成,并拒绝保存更改。然后仅为了确认一切正常,重新播放此测试。这是为了确保Squish为对象映射添加必要的名称,因为这样做可能比使用小偷对每个感兴趣的对象都更快。在重放了模拟测试后,如果您想的话可以删除它。
在我们需要的所有对象名称都出现在对象映射中后,现在我们可以完全从头开始编写自己的测试脚本。我们将从main
函数开始,然后我们将查看main
函数使用的辅助函数。
import names def main(): startApplication("AddressBook.jar") invokeMenuItem("_File", "_New...") tableView = waitForObject({"id": "itemTbl", "styletype": "table-view"}) test.verify(tableView.items.length == 0) data = [("Andy", "Beach", "[email protected]", "555 123 6786"), ("Candy", "Deane", "[email protected]", "555 234 8765"), ("Ed", "Fernleaf", "[email protected]", "555 876 4654")] for fields in data: addNameAndAddress(fields) test.verify(tableView.items.length == len(data)) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication("AddressBook.jar"); activateItem(waitForObjectItem(names.addressBookMenuBar, "_File")); activateItem(waitForObjectItem(names.addressBookFileContextMenu, "_New...")); table = waitForObject({"id": "itemTbl", "styletype": "table-view"}); test.verify(table.items.length == 0); var data = [ ["Andy", "Beach", "[email protected]", "555 123 6786"], ["Candy", "Deane", "[email protected]", "555 234 8765"], ["Ed", "Fernleaf", "[email protected]", "555 876 4654"]]; for (var row = 0; row < data.length; ++row) { addNameAndAddress(data[row]); } test.compare(table.items.length, data.length); closeWithoutSaving(); }
require 'names.pl'; sub main { startApplication("AddressBook.jar"); invokeMenuItem("_File", "_New..."); my $tableView = waitForObject({"id" => "itemTbl", "styletype" => "table-view"}); test::verify($tableView->items->length == 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 $details (@data) { addNameAndAddress(@{$details}); } test::verify($tableView->items->length == scalar(@data)); closeWithoutSaving(); }
require 'names' include Squish def main startApplication("AddressBook.jar") invokeMenuItem("_File", "_New...") tableView = waitForObject({:id => "itemTbl", :styletype => "table-view"}) Test.verify(tableView.items.length == 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(tableView.items.length, data.length) closeWithoutSaving end
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "AddressBook.jar" invokeMenuItem "_File" "_New..." set tableView [waitForObject [::Squish::ObjectName id itemTbl styletype table-view]] test compare [property get [property get $tableView items] length] 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 [property get [property get $tableView items] length] [llength $data] closeWithoutSaving }
我们首先通过调用 ApplicationContext startApplication(autName) 函数来启动应用程序。我们传递的字符串是Squish中注册的名字(通常是包含应用程序代码的 .jar
文件名或包含 main
方法的类的 .class
文件名)。然后我们获取对 TableView
的引用。我们想要的对象名尚未在对象图中,而从 tst_general 中存在的 TableView
的对象名过于具体,此时在测试中将不起作用。因此,我们可以使用同一类型的现有符号名,在编辑器中右键单击它并 将其转换为真实名称,移除对功能无关或过于具体的属性。
Object waitForObject(objectOrName) 函数会等待一个对象准备就绪(可见并启用)并返回其引用——或者超时并抛出可捕获的异常。
invokeMenuItem
函数是我们专门为这次测试创建的。它接收一个菜单名和一个菜单选项名并调用菜单选项。使用 invokeMenuItem
函数执行 文件 > 新建 后,我们验证表格的 items.length 为 0。在只想验证一个条件是真的,而不是比较两个不同值的情况下,Boolean test.verify(condition) 函数非常有用。对于 Tcl,我们通常使用 Boolean test.compare(value1, value2) 函数而不是 Boolean test.verify(condition) 函数,因为在使用 Tcl 时它稍微简单一些。
接下来,我们创建一些样本数据并调用自定义 addNameAndAddress
函数,使用 AUT 的添加对话框将数据填充到表格中。然后我们再次比较 TableView
的 items.length,这次是要与我们的样本数据中的行数进行比较。最后,我们调用自定义 closeWithoutSaving
函数以终止应用程序。
现在,我们将回顾这三个支持函数中的每一个,以涵盖 tst_adding
测试案例中的所有代码,首先是从 invokeMenuItem
函数开始。
def invokeMenuItem(menu, item): activateItem(waitForObjectItem({"styletype": "menu-bar"}, menu)) activateItem(waitForObjectItem({"caption" : menu, "type" : 'javafx.scene.control.ContextMenu'}, item))
function invokeMenuItem(menu, item) { activateItem(waitForObjectItem({"styletype": "menu-bar"}, menu)); activateItem(waitForObjectItem({"type": 'javafx.scene.control.ContextMenu', "caption": menu}, item)); }
sub invokeMenuItem { my($menu, $item) = @_; activateItem(waitForObjectItem({"styletype" => "menu-bar"}, $menu)); activateItem(waitForObjectItem({"caption" => $menu, "type" => 'javafx.scene.control.ContextMenu'}, $item)); }
def invokeMenuItem(menu, item) activateItem(waitForObjectItem({:styletype => "menu-bar"}, menu)) activateItem(waitForObjectItem({:caption => menu, :type => "javafx.scene.control.ContextMenu"}, item)) end
proc invokeMenuItem {menu item} { invoke activateItem [waitForObjectItem [::Squish::ObjectName styletype menu-bar] $menu] invoke activateItem [waitForObjectItem [::Squish::ObjectName caption $menu type javafx.scene.control.ContextMenu] $item] }
如我们前面提到的,Squish 用于菜单、菜单项(以及其他对象)的对象名会根据上下文而变化,而且通常是从窗口的标题派生出来的。对于将当前文件名放入标题中的应用程序(例如地址簿示例),名称将包含文件名,并且出于我们的测试目的,我们想忽略这个名称。
在地址簿示例的情况下,Stage
的标题 "地址簿"(启动时)、"地址簿 - 未命名"(在 文件 > 新建 之后但在 文件 > 保存 或 文件 另存为 之前),以及 "地址簿 - filename",其中 filename 当然可以变化。我们的代码通过使用真实(多属性)名称来处理所有这些情况。
真实名称由脚本语言中的键值映射表示。每个真实名称都必须指定一个类型或 styletype 属性,通常至少还有一个其他属性。在这里,我们使用 styletype 来唯一标识 MenuBar
,并使用类型和 caption 属性来唯一标识 ContextMenu
。
一旦我们识别出要与之交互的对象,我们就使用Object waitForObjectItem(objectOrName, itemOrIndex)函数来获取其对引用,并在本例中应用activateItem(objectOrName, itemText)函数。功能Object waitForObjectItem(objectOrName, itemOrIndex),暂停Squish,直到指定的对象及其项可见并启用。所以,这里我们等待着菜单栏和其中一个菜单栏项的出现,然后等待上下文菜单的出现。每次等待结束后,我们通过activateItem(objectOrName, itemText)函数激活对象及其项.
def addNameAndAddress(fields): invokeMenuItem("_Edit", "_Add...") type(waitForObject(names.address_Book_Add_forenameText_TextField), fields[0]) type(waitForObject(names.address_Book_Add_surnameText_TextField), fields[1]) type(waitForObject(names.address_Book_Add_emailText_TextField), fields[2]) type(waitForObject(names.address_Book_Add_phoneText_TextField), fields[3]) mouseClick(waitForObject(names.address_Book_Add_OK_Button), 35, 7, 0, Button.Button1)
function addNameAndAddress(fields) { invokeMenuItem("_Edit", "_Add..."); type(waitForObject(names.addressBookAddForenameTextTextField), fields[0]); type(waitForObject(names.addressBookAddSurnameTextTextField), fields[1]); type(waitForObject(names.addressBookAddEmailTextTextField), fields[2]); type(waitForObject(names.addressBookAddPhoneTextTextField), fields[3]); mouseClick(waitForObject(names.addressBookAddOKButton), 18, 18, 0, Button.Button1); }
sub addNameAndAddress { invokeMenuItem("_Edit", "_Add..."); type(waitForObject($Names::address_book_add_forenametext_textfield), $_[0]); type(waitForObject($Names::address_book_add_surnametext_textfield), $_[1]); type(waitForObject($Names::address_book_add_emailtext_textfield), $_[2]); type(waitForObject($Names::address_book_add_phonetext_textfield), $_[3]); mouseClick(waitForObject($Names::address_book_add_ok_button), 36, 14, 0, Button->Button1); }
def addNameAndAddress(oneNameAndAddress) invokeMenuItem("_Edit", "_Add...") mouseClick(waitForObject(Names::Address_Book_Add_forenameText_TextField)) type(waitForObject(Names::Address_Book_Add_forenameText_TextField), oneNameAndAddress[0]) type(waitForObject(Names::Address_Book_Add_surnameText_TextField), oneNameAndAddress[1]) type(waitForObject(Names::Address_Book_Add_emailText_TextField), oneNameAndAddress[2]) type(waitForObject(Names::Address_Book_Add_phoneText_TextField), oneNameAndAddress[3]) mouseClick(waitForObject(Names::Address_Book_Add_OK_Button), 35, 15, 0, Button::BUTTON1) end
proc addNameAndAddress {fields} { invokeMenuItem "_Edit" "_Add..." invoke type [waitForObject $names::Address_Book_Add_forenameText_TextField] [lindex $fields 0] invoke type [waitForObject $names::Address_Book_Add_surnameText_TextField] [lindex $fields 1] invoke type [waitForObject $names::Address_Book_Add_emailText_TextField] [lindex $fields 2] invoke type [waitForObject $names::Address_Book_Add_phoneText_TextField] [lindex $fields 3] invoke mouseClick [waitForObject $names::Address_Book_Add_OK_Button] 32 11 0 [enum Button Button1] }
对于每一组名称和地址数据,我们调用编辑 > 添加菜单选项来弹出添加对话框。然后,对于接收到的每个值,我们通过等待相关的TextField
准备好并使用type(objectOrName, text)函数输入文本,来填充适当的字段。最后,我们点击对话框的确定按钮。我们通过从记录的tst_general
测试中复制它并简单地通过文本参数化得到函数的核心代码。同样,我们从tst_general
测试用例的代码中复制了单击确定按钮的代码。
def closeWithoutSaving(): invokeMenuItem("_File", "_Quit") mouseClick(waitForObject(names.address_Book_No_Button), 21, 11, 0, Button.Button1)
function closeWithoutSaving() { invokeMenuItem("_File", "_Quit"); mouseClick(waitForObject(names.addressBookNoButton), 26, 14, 0, Button.Button1); }
sub closeWithoutSaving { invokeMenuItem("_File", "_Quit"); mouseClick(waitForObject($Names::address_book_no_button), 22, 17, 0, Button->Button1); }
def closeWithoutSaving invokeMenuItem("_File", "_Quit") mouseClick(waitForObject(Names::Address_Book_No_Button), 22, 13, 0, Button::BUTTON1) end
proc closeWithoutSaving {} { invokeMenuItem "_File" "_Quit" invoke mouseClick [waitForObject $names::Address_Book_No_Button] 19 18 0 [enum Button Button1] }
在这里,我们使用invokeMenuItem
函数来执行文件 > 退出,然后点击“保存未保存更改”对话框的否按钮。
整个测试代码少于30行——如果我们把一些常用的函数(如invokeMenuItem
和closeWithoutSaving
)放入一个共享脚本中,行数会更少。而且,大部分代码直接从录制的测试中复制而来,有些地方则进行了参数化。
这应该足够给人一个编写AUT测试脚本的感受。记住,Squish提供的功能远不止我们所使用的,所有这些都在API参考和工具参考中有覆盖。并且Squish还提供了访问AUT对象的所有公共API。
然而,测试案例的一个方面不太令人满意。虽然用我们这里的方式嵌入测试数据对于小量数据来说是合理的,但它相当有限,尤其是当我们想使用大量的测试数据时。此外,我们还没有测试添加到其中的数据是否正确地出现在TableView
中。在下一节中,我们将创建这个测试的新版本,这次我们将从外部数据源中提取数据,并检查TableView
中的数据是否正确。
创建数据驱动测试
在上一节中,我们在测试中放入了三个硬编码的名称和地址。但是如果我们想测试很多数据怎么办?一个方法是导入一个数据集到Squish中,并将数据集作为我们的测试中插入值的来源。Squish可以导入.tsv
(制表符分隔值格式).csv
(逗号分隔值格式)、.xls
或.xlsx
(Microsoft Excel电子表格格式)。
注意:假设.csv
和.tsv
文件使用Unicode UTF-8编码——与所有测试脚本相同的编码。
测试数据可以通过使用squishide
导入,或者使用文件管理器或控制台命令手动导入。我们将描述这两种方法,首先是使用squishide
的方法。
对于地址簿应用程序,我们想要导入MyAddresses.tsv
数据文件。您可以在SQUISHDIR/examples/java/addressbook_fx/suite_xyz/shared/testdata
找到该文件的副本。我们首先使用文件 > 导入测试资源以弹出导入Squish资源对话框。在对话框内部,点击浏览按钮选择要导入的文件——在这种情况下是MyAddresses.tsv
。确保将导入为组合框设置为“测试数据”。默认情况下,squishide
将仅导入当前测试用例的测试数据,但我们需要所有测试套件测试用例都可用测试数据:要这样做,请选中复制到测试套件以共享单选按钮。现在点击完成按钮。您现在可以在测试套件资源视图(在测试数据选项卡)中看到文件列表,如果您单击文件名,它将在编辑视图中显示。截图显示了在添加测试数据后Squish的状态。
要从squishide
外导入测试数据,请使用文件管理器,如文件资源管理器或查找器,或者使用控制台命令。在测试套件目录中创建一个名为shared
的目录。然后,在shared
目录中创建一个名为testdata
的目录。将数据文件(在本例中为MyAddresses.tsv
)复制到shared\testdata
目录中。
如果squishide
正在运行,请重新启动。如果您点击测试套件资源视图的测试数据选项卡,您应该看到数据文件。单击文件名称以在编辑视图中查看文件。
尽管在实际生活中我们会修改我们的tst_adding
测试用例以使用测试数据,但为了本教程的目的,我们将创建一个新的测试用例tst_adding_data
,它是tst_adding
的副本,我们将修改它以使用测试数据。
唯一需要更改的函数是main
,其中我们不是遍历硬编码的数据项,而是遍历数据集中的所有记录。我们还需要更新最后的期望行数,因为我们现在增加了更多的记录,我们还将添加一个函数来验证每个添加的记录。
import names def main(): startApplication("AddressBook.jar") invokeMenuItem("_File", "_New...") tableView = waitForObject({"id": "itemTbl", "styletype": "table-view"}) test.compare(tableView.items.length, 0) limit = 4 # 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)) checkNameAndAddress(tableView, record) if row >= limit: break test.compare(tableView.items.length, row + 1) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication("AddressBook.jar"); activateItem(waitForObjectItem(names.addressBookMenuBar, "_File")); activateItem(waitForObjectItem(names.addressBookFileContextMenu, "_New...")); table = waitForObject({"id": "itemTbl", "styletype": "table-view"}); test.verify(table.items.length == 0); var limit = 10; // To avoid testing 100s of rows since that would be boring var records = testData.dataset("MyAddresses.tsv"); var row = 0; for (; 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([forename, surname, email, phone]); checkNameAndAddress(table, record); if (row > limit) break; } test.compare(table.items.length, row + 1); closeWithoutSaving(); }
require 'names.pl'; sub main { startApplication("AddressBook.jar"); invokeMenuItem("_File", "_New..."); my $tableView = waitForObject({"id" => "itemTbl", "styletype" => "table-view"}); test::verify($tableView->items->length == 0); my $limit = 4; # 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); checkNameAndAddress($tableView, $record); if ($row >= $limit) { last; } } test::compare($tableView->items->length, $row + 1); closeWithoutSaving(); }
require 'names' include Squish def main startApplication("AddressBook.jar") invokeMenuItem("_File", "_New...") tableView = waitForObject({:id => "itemTbl", :styletype => "table-view"}) Test.verify(tableView.items.length == 0) limit = 4 # 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]) # pass as a single Array checkNameAndAddress(tableView, record) break if row >= limit rows += 1 end Test.compare(tableView.items.length, rows + 1) closeWithoutSaving end
source [findFile "scripts" "names.tcl"] proc main {} { startApplication "AddressBook.jar" invokeMenuItem "_File" "_New..." set tableView [waitForObject [::Squish::ObjectName id itemTbl styletype table-view]] test compare [property get [property get $tableView items] length] 0 # To avoid testing 100s of rows since that would be boring set limit 4 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 fields [list $forename $surname $email $phone] addNameAndAddress $fields checkNameAndAddress $tableView $record if {$row >= $limit} { break } } test compare [property get [property get $tableView items] length] [expr $row + 1] closeWithoutSaving }
Squish通过其testData
模块的函数提供对测试数据的访问——在这里我们使用了Dataset testData.dataset(filename)函数来访问数据文件并将记录提供出来,以及String testData.field(record, fieldName)函数来检索每个记录的各个字段。
使用测试数据填充TableView
后,我们想要确保表中的数据与我们添加的一致,因此我们添加了checkNameAndAddress
函数。我们还添加了一个记录比较的上限,这样可以使测试运行更快。
def checkNameAndAddress(tableView, record): tableModel = tableView.getColumns() for column in range(len(testData.fieldNames(record))): value = tableModel.get(column).getCellData(0).toString() test.compare(value, testData.field(record, column))
function checkNameAndAddress(tableView, record) { var tableModel = tableView.getColumns(); for (var column = 0; column < testData.fieldNames(record).length; ++column) { var value = tableModel.get(column).getCellData(0).toString(); test.compare(value, testData.field(record, column)); } }
sub checkNameAndAddress { my($tableView, $record) = @_; my $tableModel = $tableView->getColumns(); my @columnNames = testData::fieldNames($record); for (my $column = 0; $column < scalar(@columnNames); $column++) { my $value = $tableModel->get($column)->getCellData(0)->toString(); test::compare($value, testData::field($record, $column)); } }
def checkNameAndAddress(tableView, record) tableModel = tableView.getColumns() for column in 0...TestData.fieldNames(record).length value = tableModel.get(column).getCellData(0).toString() Test.compare(value, TestData.field(record, column)) end end
proc checkNameAndAddress {tableView record} { set tableModel [invoke $tableView getColumns] set columns [llength [testData fieldNames $record]] for {set column 0} {$column < $columns} {incr column} { set value [toString [invoke [invoke $tableModel get $column] getCellData 0]] test compare $value [testData field $record $column] } }
此函数访问TableView的底层TableModel,并提取每个单元格的值。然后我们使用Squish的Boolean test.compare(value1, value2)函数检查单元格中的值是否与我们用于测试的数据中的值相同。请注意,此特定AUT总是在当前行之前添加新行(如果没有行,则作为第一行),并将添加的行设置为当前行。这种效果是,每个新的姓名和地址总是作为第一行添加,这就是我们为什么将行硬编码为0。
截图显示了运行数据驱动的测试后的Squish测试摘要日志。
Squish还可以进行关键字驱动测试。这比数据驱动测试复杂一些。请参阅如何进行关键字驱动测试。
Java示例应用程序及其相应的测试在SQUISHDIR/examples/java
中提供。
©2024 Qt公司有限公司。此处包含的文档贡献是各自所有者的版权。
此处提供的文档根据自由软件基金会发布的GNU自由文档许可证版本1.3的条款进行许可。
Qt及其相应的徽标是Qt公司在芬兰和其他国家的商标。所有其他商标均为各自所有者的财产。