适用于 macOS 的 Squish 教程
学习如何测试本地产生 macOS 应用程序。
教程:开始测试macOS应用程序
Squish自带IDE和命令行工具。使用squishide
是最简单也是最理想的方法来开始,但是一旦你建立了很多测试,你可能希望自动化它们。例如,为了执行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或shell脚本中运行的命令行工具是有意义的。
注意:如果您需要视频指导,可以在的Qt Academy中找到一个关于Squish基本使用的45分钟在线课程。
在本章中,我们将使用一个简单的地址簿应用程序作为我们的应用被测试(AUT)。该应用程序包含在Squish的SQUISHDIR/examples/mac/addressbook
中。这是一个非常基本的Cocoa应用程序,允许用户加载现有的地址簿或创建一个新的,添加、编辑和删除条目,以及保存(或另存为)新的或修改后的地址簿。尽管应用程序很简单,但它具有大多数标准应用都具备的所有关键功能:一个菜单栏带有下拉菜单、一个工具栏,以及一个中央区域——在这个例子中显示为表格。它支持原地编辑,还有一个用于添加项目的表单。您可以轻松地将您在本应用测试中学到的所有概念和方法应用到您自己的应用中。有关测试各种Cocoa特定功能的更多示例,请参阅如何创建测试脚本。
注意:在整个手册中,我们经常提及SQUISHDIR
目录。这意味着Squish安装的目录,可能是C:\Squish
、/usr/local/squish
、/opt/local/squish
等,具体取决于您的安装位置。确切的位置并不重要,只要您在看到路径和文件名时心里将其转换为真实的目录即可。
此截图显示了应用程序与新创建的空地址簿。
使用示例
您第一次尝试运行示例AUT的测试时,可能会得到一个以Squish找不到要启动的应用被测试...开始的致命错误。为了从错误中恢复,点击测试套件设置工具栏按钮,然后在应用被测试(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,该AUT启动了Squish钩子。钩子是一个小型的库,使AUT的实时运行对象可访问,并允许与squishserver通信。有了钩子的存在,squishserver可以查询AUT对象的状态,并代表squishrunner执行命令。squishrunner指导AUT执行测试脚本指定的任何操作。
所有通信都通过网络套接字进行,这意味着可以在单个计算机上进行一切操作,或者测试脚本可以在一台计算机上执行,而被测软件可以在网络上的另一台计算机上进行测试。
下面的图示说明了单个Squish工具是如何协作工作的。
从测试工程师的角度来看,这种分离并不明显,因为所有通信都隐秘地处理。
可以使用squishide
编写和执行测试,在这种情况下,squishserver会自动启动和停止,并且在squishide
的测试结果视图中显示测试结果。下面的图示说明了当使用squishide
时幕后发生的事情。
Squish工具也可以在不需要squishide
的情况下从命令行使用。如果您更喜欢使用自己的工具,例如您最喜欢的编辑器,或者想要执行自动批量测试,这很有用。例如,当在夜间进行回归测试时。在这些情况下,必须手动启动squishserver,或者在所有测试完成后停止,或者为每个测试启动和停止。
注意: Squish文档大多使用术语小部件来指代GUI对象,例如按钮、菜单、菜单项、标签和表格控件。Windows用户可能更熟悉术语控件和容器,但在这里我们使用术语小部件来指代两者。类似地,macOS用户可能习惯于术语视图。
创建测试套件
测试套件是一组一个或多个测试用例(测试)。使用测试套件很方便,因为它使得在相关测试组之间共享脚本和测试数据变得很容易。
在这里,以及在整个教程中,我们首先将描述如何使用squishide
做事情,而对于命令行用户,以下是信息。
从squishide
创建测试套件
启动 squishide
—可以通过单击或双击squishide图标、通过任务栏菜单启动squishide或通过命令行执行squishide的方式进行。请根据您喜好和适用平台选择合适的方式。Squish启动后,可能会出现一个欢迎页面。点击右上角的工作台按钮以关闭它。然后,squishide的界面看起来将与截图类似,但可能因窗口系统、颜色、字体和主题而略有不同。
Squish启动后,点击文件 > 新建测试套件,即可弹出下面的新建测试套件向导。
输入您的测试套件名称并选择存储测试套件的文件夹。在截图中,我们已将测试套件命名为suite_py
,并将其放置在addressbook
文件夹内。(对于您的自用测试,您可以使用更具有意义的名称,例如"suite_addressbook";我们选择"suite_py",因为在本教程中,我们将为Squish支持的每种脚本语言创建一个测试套件。)当然,您可以选择您喜欢的任何名称和文件夹。完成详细信息后,点击下一步以转到工具集(或脚本语言)页面。
如果您看到此向导页面,请单击您的AUT使用的工具集。在本例中,我们必须单击Mac,因为我们正在测试macOS应用程序。然后点击下一步以转到脚本语言页面。
选择您想要的任意脚本语言——唯一限制是每个测试套件只能使用一种脚本语言。(因此,如果您想使用多种脚本语言,只需创建多个测试套件,每个测试套件针对您想使用的各种脚本语言。)Squish支持的所有语言的 功能性都相同。选择脚本语言后,再点击一次下一步以到达向导最后一步。
如果您为Squish已知的AUT创建新的测试套件,只需单击下拉列表,从中选择AUT即可。如果下拉列表为空或您的AUT未列出来,点击下拉列表右侧的浏览按钮——这将从文件打开对话框中弹出,您从该对话框中选择AUT。在Cocoa程序的情况下,AUT是应用程序的可执行文件(例如,macOS上的SquishAddressBook
)。选择AUT后,点击完成,Squish将在包含与测试套件相同名称的子文件夹中创建一个名为suite.conf
的文件,其中包含测试套件的配置详细信息。Squish还将AUT注册到squishserver中。然后向导将关闭,squishide
将看起来类似于下面的截图。
我们现在可以开始创建测试。继续阅读以了解如何在不使用squishide
的情况下创建测试套件,或跳转到记录测试和验证点。
从命令行创建测试套件
要使用命令行创建新的测试套件
- 创建一个新目录来存储测试套件——目录的名称应以
suite_
开头。在本例中,我们为Python测试创建了SQUISHDIR/examples/mac/addressbook/suite_py
目录。(我们还有为其他语言提供类似的子目录,但这只是为了示例,因为我们通常只针对所有测试使用一种语言。) - 将AUT与squishserver注册。
注意:每个自动测试工具(AUT)都必须在squishserver中注册,以便测试脚本不需要包含AUT的路径,从而实现测试的平台无关性。注册的另一个好处是,可以在不使用
squishide
的情况下测试AUT——例如,在进行回归测试时。这可以通过在命令行上使用具有
--config
选项和addAUT
命令的squishserver来实现。例如,假设我们位于macOS中的SQUISHDIR
目录squishserver --config addAUT SquishAddressBook \ SQUISHDIR/examples/mac/addressbook
我们必须向
addAUT
命令提供AUT的可执行文件名,以及——分开地——AUT的路径。在这种情况下,路径是指测试套件配置文件中添加为AUT的可执行文件。有关应用程序路径的更多信息,请参见AUTs and Settings,有关squishserver的命令行选项,请参见squishserver。 - 在测试套件的子目录中创建一个名为
suite.conf
的纯文本文件(ASCII或UTF-8编码)。这是测试套件的配置文件,并且至少必须标识AUT、用于测试的脚本语言以及AUT使用的包装器(即GUI工具包或库)。文件的格式是key
=
value
,每行一个键值对。例如AUT = SquishAddressBook LANGUAGE = Python WRAPPERS = Mac OBJECTMAPSTYLE = script
AUT是之前步骤中注册的Cocoa可执行文件。LANGUAGE可以设置为任何你喜好的语言——目前Squish能够支持JavaScript、Python、Perl、Ruby和Tcl。WRAPPERS应设置为Mac。
记录测试和验证点
Squish使用为测试套件指定的脚本语言来记录测试。一旦记录了一个测试,我们就可以运行这个测试,Squish将忠实地重复我们在记录测试时执行的所有操作,但没有人类易于犯的暂停。也可以编辑记录的测试,或者将记录的测试的一部分复制到手动创建的测试中,就像我们将在教程的后面看到的那样。
将这些录制到现有的测试用例中。您可以通过以下方式创建一个新的脚本测试用例:
- 选择文件 > 新测试用例以打开新Squish测试用例向导,输入测试用例的名称,然后选择完成。
- 点击新脚本测试用例()工具栏按钮,位于测试用例下方的测试套件视图中。这将创建一个新的测试用例,具有默认名称,您可以轻松更改。
给新的测试用例命名为"tst_general"。
Squish将自动在测试套件文件夹内创建一个同名子文件夹,并创建一个测试文件,例如test.py
。如果您选择JavaScript作为脚本语言,文件名为test.js
,对于Perl、Ruby或Tcl也相应。
如果您得到的是示例.feature
文件而不是“Hello World”脚本,点击运行测试套件()左侧的箭头,然后选择新脚本测试用例()。
要使测试脚本文件(例如,test.js
或 test.py
)出现在 编辑器视图 中,根据 首选项 > 通用 > 打开模式 设置,单击或双击测试用例。这会将脚本选为活动脚本,并显示其对应的 记录 () 和 运行测试 () 按钮。
复选框用于在点击 运行测试套件 () 工具栏按钮时控制运行哪些测试用例。我们还可以通过单击其 运行测试 () 按钮来运行单个测试用例。如果测试用例当前未处于活动状态,按钮可能不可见,直到鼠标悬停在其上。
最初,脚本的 main()
将 你好,世界 输出到测试结果。要手动创建测试,就像我们在教程中稍后要做的那样,我们 必须 创建一个 main
函数,并且应该将相同的导入放在顶部。对于 Squish 来说,main
这个名字是特别的。测试可以包含尽可能多的函数和其他代码,这些代码由脚本语言支持,但在测试执行(即运行)时,Squish 总是执行 main
函数。您可以在测试脚本之间共享常用代码,如 如何创建和使用共享数据和共享脚本 中所述。
还有两个函数名对 Squish 也是特别的:cleanup
和 init
。有关更多信息,请参阅 测试师创建的特殊函数。
一旦创建了一个新的测试用例,我们就可以手动编写测试代码或录制一个测试。单击测试用例的 记录 () 按钮将用新的录制替换测试的代码。或者,您还可以根据 如何编辑和调试测试脚本 中的说明来录制代码片段并插入到现有测试用例中。
从命令行创建测试
要从命令行创建新的测试用例
- 在测试套件目录内创建一个新的子目录。例如,在
SQUISHDIR/examples/mac/addressbook/suite_py
目录内,创建tst_general
目录。 - 在测试用例的目录内创建一个名为
test.py
的文件(如果您使用的是 JavaScript 脚本语言,则为test.js
,以及其他语言的类似情况)。
录制我们的第一个测试
在我们开始录制之前,让我们简要回顾一下我们非常简单的测试场景:
- 打开
MyAddresses.adr
地址文件。 - 导航到第二个地址并添加一个新的姓名和地址。
- 导航到第四个地址(即是第三个地址)并更改姓氏字段。
- 导航到第一个地址并删除它。
- 验证第一个地址是否现在添加的新地址。
我们现在可以录制我们的第一个测试了。单击 测试套件视图 中的测试用例列表显示的 tst_general
测试用例旁边的 记录 () 按钮。这将导致 Squish 运行 AUT 以便我们可以与之交互。一旦 AUT 执行,请执行以下操作——不要担心所需的时间,因为 Squish 不记录空闲时间。
- 单击文件 > 打开,当文件对话框出现时,按Shift+ +Command+g并在出现的行编辑中输入
MyAddresses.adr
文件名,然后单击转到按钮,再单击打开按钮。 - 单击第一行,然后单击工具栏上的添加按钮,接着在添加工作表的第 一行编辑框中输入 "Jane"。现在点击(或按Tab键转到)第二行编辑框并输入 "Doe"。类似的操作,设置一个电子邮件地址为 "[email protected]" 和一个电话号码为 "555 123 4567"。无需担心输入错误——正常使用退格键删除并更正即可。最后,单击添加按钮。现在应该有一个新的第二地址,包含了您输入的详细信息。
- 双击第四行的第二个(姓氏)列,删除其文本并将其替换为 "Doe"。 (您可以通过简单地进行覆盖输入然后按
键即可完成。) - 单击第一行,然后单击工具栏上的删除按钮,然后在消息框中单击删除按钮。第一行应该已消失,这样您的 "Jane Doe" 条目现在就是第一个。
- 在 Squish 控制栏中,单击验证工具栏按钮,然后从下拉菜单中选择属性。
这会使
squishide
出现,中心小部件应该显示验证点创建器视图。在选择任何属性之前,请确保VP的类型是 脚本化属性,如图所示。在应用程序对象视图中,展开
地址簿 - MyAddresses.adr_NSWindow_0
对象,然后是NSView_0
对象,接着是NSScrollView_0
对象,然后是NSClipView_0
对象,最后是NSTableView_0
对象。单击Jane_NSCFString_0
对象,使其属性出现在属性视图中,然后检查stringValue
属性的复选框。现在向下滚动并单击Doe_NSCFString_127
对象并检查其stringValue
属性。最后,单击底部的保存并插入验证按钮(在验证点创建器的底部)将第一行的姓名和姓氏验证插入到记录的测试脚本中。 (参见图表。)一旦插入验证点,squishide
的窗口将再次隐藏,控制栏窗口和AUT将再次显示在视图上。 - 现在我们已经完成了测试,所以可以在AUT中单击SquishAddressBook > 退出,然后在消息框中单击不保存,因为我们不想保存任何更改。
退出AUT后,记录的测试将如截图所示出现在squishide
中。(请注意,记录的确切代码将根据您如何交互而有所不同。例如,您可能通过单击它们或使用快捷键序列来调用菜单选项——无论如何使用,Squish都会以不同的方式记录。)
如果记录的测试没有出现,请单击(或根据您的平台和设置单击或双击)tst_general
测试用例;这将使Squish以截图所示的窗口显示测试的test.py
文件。
现在我们记录了测试,我们能够播放它,即运行它。这本身很有用,因为如果回放失败,它可能意味着应用程序已被破坏。此外,我们在回放中检查了我们放入的两个验证,如图表所示。
在测试录制过程中添加验证点非常方便。这里我们一次性添加了两个,但实际上,我们可以在测试录制过程中随时添加任何数量的验证点。然而,有时我们可能会忘记添加一个验证点,或者在之后我们可能想要添加一个新的验证点。正如我们在下一节中将要看到的,我们可以轻松地在录制好的测试脚本中添加额外的验证点,见添加额外的验证点。
在进一步讲解之前,我们将查看如何从命令行录制测试。然后我们将查看如何运行测试,并且我们还将查看Squish在录制测试时生成的部分代码,并讨论一些其功能。
从命令行录制测试
在录制测试时,squishserver必须始终运行。这由squishide
自动处理,但命令行用户必须根据squishserver中的说明手动启动squishserver。
要从命令行录制测试,执行squishrunner程序,并指定要录制的测试套件的名称和测试用例的名称。例如,假设我们处于包含测试套件目录的目录中:
squishrunner --testsuite suite_py --record tst_general --useWaitFor
最好使用--useWaitFor
选项来录制对Object waitForObject(objectOrName)函数的调用,这比使用snooze(seconds)函数更加可靠,尽管它因为历史原因已经成为默认设置。squishide
会自动使用Object waitForObject(objectOrName)函数。
当连接多个设备或模拟器时,您需要使用--device some-device
指定目标。
从IDE运行测试
要在squishide
中运行测试用例,当测试用例被悬停或选中时,点击测试套件视图中的运行测试()。
要顺序运行两个或更多测试用例,或者只运行选定的测试用例,请点击运行测试套件()。
从命令行运行测试
运行测试时,squishserver必须始终运行,或者必须向squishrunner提供--local
选项。有关更多信息,请参见squishserver。
要从命令行播放已录制的测试,我们执行squishrunner程序并指定包含已录制脚本的测试套件和我们要播放的测试用例。例如,假设我们处于包含测试套件目录的目录中:
squishrunner --testsuite suite_py --testcase tst_general --local
检查生成的代码
如果您查看截图中的代码(或以下显示的代码片段),您会看到它由许多Object waitForObject(objectOrName)调用组成,这些调用是其他各种调用的参数,如activateItem(objectOrName)、mouseClick(objectOrName)、以及type(objectOrName, text)。该Object waitForObject(objectOrName)函数等待GUI对象准备好进行交互(即变为可见并启用),然后随后是一些与对象交互的函数。典型交互包括激活(弹出)菜单、单击菜单选项或按钮,或输入一些文本。
有关Squish脚本命令的完整概述,请参阅如何创建测试脚本、如何测试应用程序 - 特定细节、API参考和工具参考。
对象通过Squish生成的名称进行识别。有关详细信息,请参阅如何识别和访问对象。
生成的代码大约有35行。以下是一个摘录,仅显示Squish如何记录单击添加工具栏按钮、在添加表中输入Jane Doe的详细信息,以及最后单击添加以关闭表并更新表格的操作。
注意:尽管截图只显示了Python测试套件的运行情况,但在这里和整个教程中引用的代码片段,我们展示了Squish支持的所有脚本语言的代码。当然,在实际操作中,您只会使用其中之一,因此请随意查看您感兴趣的代码片段,并跳过其他代码。
mouseClick(waitForObject(names.address_Book_MyAddresses_adr_Add_NSToolbarItem)) type(waitForObject(names.add_Address_NSTextField), "Jane") type(waitForObject(names.add_Address_NSTextField), "<Tab>") type(waitForObject(names.add_Address_NSTextField_2), "Doe") type(waitForObject(names.add_Address_NSTextField_2), "<Tab>") type(waitForObject(names.add_Address_NSTextField_3), "[email protected]") type(waitForObject(names.add_Address_NSTextField_3), "<Tab>") type(waitForObject(names.add_Address_NSTextField_4), "555 123 4567") mouseClick(waitForObject(names.add_Address_Add_NSButton))
mouseClick(waitForObject(names.addressBookMyAddressesAdrAddNSToolbarItem)); type(waitForObject(names.addAddressNSTextField), "Jane"); type(waitForObject(names.addAddressNSTextField), "<Tab>"); type(waitForObject(names.addAddressNSTextField_2), "Doe"); type(waitForObject(names.addAddressNSTextField_2), "<Tab>"); type(waitForObject(names.addAddressNSTextField_3), "[email protected]"); type(waitForObject(names.addAddressNSTextField_3), "<Tab>"); type(waitForObject(names.addAddressNSTextField_4), "555 123 4567"); mouseClick(waitForObject(names.addAddressAddNSButton));
mouseClick(waitForObject($Names::address_book_myaddresses_adr_add_nstoolbaritem)); type(waitForObject($Names::add_address_nstextfield), "Jane"); type(waitForObject($Names::add_address_nstextfield), "<Tab>"); type(waitForObject($Names::add_address_nstextfield_2), "Doe"); type(waitForObject($Names::add_address_nstextfield_2), "<Tab>"); type(waitForObject($Names::add_address_nstextfield_3), "jane.doe\@nowhere.com"); type(waitForObject($Names::add_address_nstextfield_3), "<Tab>"); type(waitForObject($Names::add_address_nstextfield_4), "555 123 4567"); mouseClick(waitForObject($Names::add_address_add_nsbutton));
mouseClick(waitForObject(Names::Address_Book_MyAddresses_adr_Add_NSToolbarItem)) type(waitForObject(Names::Add_Address_NSTextField), "Jane") type(waitForObject(Names::Add_Address_NSTextField), "<Tab>") type(waitForObject(Names::Add_Address_NSTextField_2), "Doe") type(waitForObject(Names::Add_Address_NSTextField_2), "<Tab>") type(waitForObject(Names::Add_Address_NSTextField_3), "[email protected]") type(waitForObject(Names::Add_Address_NSTextField_3), "<Tab>") type(waitForObject(Names::Add_Address_NSTextField_4), "555 123 4567") mouseClick(waitForObject(Names::Add_Address_Add_NSButton))
如您所见,测试员使用键盘从一个文本字段跳转到另一个文本字段,并使用鼠标而不是按键来单击添加按钮。如果测试员以任何其他方式单击按钮(例如,按Enter),结果将相同,但当然Squish会记录实际采取的操作。
Squish记录使用以names.
前缀开始的变量来引用对象,这些变量标识它们为符号名称。每个变量都包含一个值,即相应的真实名称,它可以是字符串-based,也可以作为属性到值的键值映射。Squish支持几种命名方案,所有这些方案都可以在脚本中使用并混合在一起。使用符号名称的优势在于,如果应用程序发生更改,需要不同的名称,我们可以简单地更新Squish的对象映射(将符号名称与真实名称相关联),从而避免需要更改我们的测试脚本。有关对象映射的更多信息,请参阅对象映射和对象映射视图。
当符号名称在光标下时,编辑器的上下文菜单允许您打开符号名称,显示其在对象映射中的条目,或转换为真实名称,这在脚本语言的光标位置放置了一个内联映射,允许您在脚本本身中手动编辑属性。
现在我们已经看到了如何记录和回放测试,并看到了Squish生成的代码,让我们再进一步,确保在测试执行中的特定点某些条件成立。
插入额外的验证点
在上一个章节中,我们看到了如何在录制测试脚本的过程中轻松插入脚本化属性验证点。验证点也可以通过设置断点和使用 squishide
,或者在测试脚本中直接编辑并调用 Squish 的测试函数(例如 Boolean test.compare(value1, value2) 和 Boolean test.verify(condition))来插入现有的测试脚本中。
Squish 支持许多种类的验证点:那些验证对象属性具有特定值(称为“对象属性验证”);那些验证整个表格内容是否符合预期(称为“表格验证”);那些验证两张图片是否相同(称为“截图验证”);以及一种混合验证类型,它包括多个对象的属性和截图(称为“视觉验证”)。此外,还可以验证搜索图像是否存在于屏幕上的某个位置,或通过 OCR 发现某些文本。最常用的种类是对象属性验证,并且在教程中我们将主要介绍这些。有关更多阅读材料,请参阅 如何创建和使用验证点。)
(普通)(非脚本化)属性验证点以 XML 文件的形式存储在测试用例或测试套件资源中,并包含要传递给 test.compare()
的值。这些验证点可以在不同的测试用例之间重用,可以在单个脚本代码行中验证多个值。
(脚本化)属性验证点是直接调用 Boolean test.compare(value1, value2) 函数,包含两个参数——特定对象的特定属性的值和期望的值。我们可以在录制或手动编写的脚本中手动插入对 Boolean test.compare(value1, value2) 函数的调用,或者使用脚本化验证点让 Squish 自动插入。在上一个章节中,我们展示了如何使用 squishide
在录制过程中插入验证。在这里,我们将首先展示如何使用 squishide
在现有测试脚本中插入验证,然后我们将展示如何手动插入验证。
在请求 Squish 插入验证点之前,最好确保我们有一个想要验证的内容及时间列表。我们可以在 tst_general
测试用例中添加许多潜在的验证,但鉴于我们在这里的目的是简单地展示如何操作,我们只会做两件事——我们将验证“Jane Doe”条目的电子邮件地址和电话号码是否与所输入的一致,并将验证点立即放置在我们刚才在录制过程中插入的验证点之后。
要使用 squishide
插入验证点,我们首先在脚本(无论录制还是手动编写——对于 Squish 来说无关紧要)中设置一个断点,在我们要验证的地方。
如上面的截图中所示,我们在脚本末尾附近设置了一个断点。这可以通过双击或在从编辑器侧边栏(位于行号旁边的侧边栏)弹出上下文菜单后选择 添加断点 来完成。我们选择此行,因为这条线紧随着移除第一个地址的脚本行,因此在这一点上(在调用文件菜单以关闭应用程序之前),第一个地址应该是“Jane Doe”的地址。截图显示了在录制过程中使用 squishide
输入的验证。我们的附加验证将跟随它们。请注意,如果您以不同的方式录制了测试,则行号可能不同,例如,使用键盘快捷键而不是点击菜单项。)
设置断点之后,我们通过点击 运行测试 () 按钮,或通过点击 运行 > 运行测试用例 菜单选项来像往常一样运行测试。与正常测试运行不同,当达到断点时,测试将停止,Squish 的主窗口将重新出现(这可能会遮挡 AUT)。在此阶段,squishide
将自动切换到 测试调试透视。
透视和视图
squishide
的工作方式与 Eclipse IDE 类似。如果你不熟悉 Eclipse,以下关键概念非常重要:视图 和 透视。在 Eclipse 中,以及因此 squishide
中,一个 视图 实际上是一个子窗口,例如一个停靠窗口或现有窗口中的一个选项卡。一个 透视 是一起排列的视图的集合。两者都可以通过 窗口 菜单访问。
squishide
内置以下透视
您可以更改这些透视以显示额外的视图或隐藏不想要的视图,或者创建具有所需视图的自己的透视。因此,如果您的窗口有很大变化,这只意味着透视已更改。使用 窗口 菜单切换到您想要的透视。然而,Squish 会自动更改透视以反映当前情况,因此您不需要手动更改透视。
插入验证点
如图下所示,当 Squish 在断点处停止时,squishide
将自动切换到 测试调试透视。该透视显示了 变量视图、编辑视图、调试视图、应用程序对象视图、属性视图、方法视图 和 测试结果视图。
要插入验证点,我们可以展开应用程序对象视图中的项目,直到找到我们想要验证的对象,或者我们可以使用 对象选择器 () 来视觉上选择它。在这个例子中,我们想要验证 NSTableView
的第一行的文本,所以我们展开 "地址簿 - MyAddresses_adr_NSWindow_0" 项目,以及其子项目,直到我们找到 NSTableView
,以及其中我们感兴趣的项目。一旦我们单击项目对象,其属性就像截图所显示的那样在 属性视图 中显示。
可以在任何时间通过从 窗口 菜单中选择它(或单击其工具栏按钮)返回正常 测试管理透视,尽管如果您停止脚本或运行到完成,squishide
将自动返回。
在这里,我们可以看到表格第一项的 stringValue
属性值为 "Jane";我们在录制过程中已经对此进行了验证。向下滚动以便您可以查看相应的(即第一个)电子邮件地址([email protected]_NSCFString_250
项)。为了确保每次测试运行时都进行验证,请在应用程序对象视图中(不需要勾选复选框)选择此对象,使它的属性出现在属性视图中,然后单击stringValue
属性以检查其复选框。勾选后,将出现验证点创建器视图,如图所示。
属性选择后,验证点尚未添加到测试脚本中。我们可以很容易地通过单击保存并插入验证按钮来添加它,但在做之前,将验证的类型设置为属性,因为组合框会记住先前选择的VP类型。此时选择一个有意义的VP名称也是一个好主意。否则我们应该得到VP1、VP2等这样的名称。
在将验证点插入脚本之前,我们还要添加一项验证事项。
向下滚动并单击第一个电话号码项(在应用程序对象视图中的 555 123 4567_NSCFString_375
项;然后在属性视图中检查其stringValue
属性。现在这两个验证点将出现在验证点创建器视图中,如图所示。
现在我们已说明我们期望这些属性具有显示的值,即电子邮件地址为 "[email protected]" 和电话号码为 "555 123 4567"。我们必须单击保存并插入验证按钮来实际插入验证点,所以现在就做吧。
我们现在不需要继续运行测试,所以我们可以通过单击工具栏上的停止按钮来停止运行测试,或者我们可以继续(通过单击恢复按钮)。
一旦我们完成了验证点的插入并停止或完成运行测试,现在应该关闭断点。只需Ctrl+点击断点,然后在上下文菜单中单击禁用断点菜单选项。现在我们已准备好运行测试,不带断点但有验证点。单击运行测试( )按钮。这一次我们将获得一些测试结果——如图所示,其中之一我们已展开以显示其详细信息。(我们还将选择Squish插入以执行验证的代码行——请注意,这里使用您选择的VP名称)。
此特定期验点存储为一个XML文件,包含对新插入的条目的电子邮件和电话号码的两个属性比较。您可以在用例资源窗口的VPs选项卡中看到它,并从那里打开它。
另一种插入验证点的方法是在代码中以代码的形式插入。理论上,我们可以在现有脚本中的任何位置添加我们自己的函数调用,例如 布尔 test.compare(value1, value2) 和 布尔 test.verify(condition)。在实际操作中,最好确保Squish首先知道我们想要验证的对象,这样在运行测试时它才能找到它们。这需要非常类似使用 squishide
的过程。首先,我们在打算添加验证的位置设置一个断点。然后运行测试脚本直到它停止。接下来,在 应用程序对象视图 中导航,直到找到我们想要验证的对象。此时,智慧的做法是 Ctrl+Click 我们感兴趣的任何对象并点击 添加到对象映射 的上下文菜单选项。这将确保Squish可以访问该对象。然后再次 Ctrl+Click 并点击 复制符号名 的上下文菜单选项——这给出了Squish将用于识别对象的名称。现在我们可以编辑测试脚本以添加我们的验证并完成或停止执行。(别忘了一旦不再需要就禁用断点。)
虽然我们可以编写测试脚本来与自动生成的代码风格完全相同,但通常以略微不同的风格操作会更容易理解。我们稍后将解释这一点。
对于我们的手动验证,我们希望在读取 MyAddresses.adr
文件、添加新地址以及最后删除第一个地址后,检查 NSTableView
中存在的地址数量。截图显示了输入以获取这三种验证之一的两行代码,以及运行测试脚本的结果。
手动编写脚本时,我们使用Squish的 test
模块的函数来验证测试脚本执行过程中的某些点。如截图(以及下面的代码片段)所示,我们首先获取我们感兴趣的引用。使用 Object waitForObject(objectOrName) 函数是手动编写的测试脚本的标准做法。此函数等待对象可用(即可见并启用),然后返回对其的引用。(否则它将超时并引发可捕获的异常。)然后我们使用此引用访问项目的属性和方法——在这种情况下是 NSTableView
的 numberOfRows
方法——并使用 布尔 test.verify(condition) 函数验证值是否如我们所期望。顺便提一下,我们从稍后的代码行中获取了对象的名称,因此我们不需要设置断点并将表格的名称手动添加到对象映射中,以确保Squish记住这一点,因为在测试记录期间它已经添加了它。)
以下是手动输入用于第一种验证的代码,适用于Squish支持的所有脚本语言。(您只需查看您将用于自己的测试的代码。对于其他验证,我们只需要调用 布尔 test.verify(condition) 函数——或者对于 Tcl,调用 布尔 test.compare(value1, value2) 函数更方便——重用我们在下面显示的代码中获得的 table
对象引用。)
table = waitForObject(names.address_Book_MyAddresses_adr_NSTableView) test.compare(table.numberOfRows(), 125)
var table = waitForObject(names.addressBookMyAddressesAdrNSTableView); test.compare(table.numberOfRows(), 125);
my $table = waitForObject($Names::address_book_myaddresses_adr_nstableview); test::compare($table->numberOfRows(), 125);
table = waitForObject(Names::Address_Book_MyAddresses_adr_NSTableView) Test.compare(table.numberOfRows(), 125)
编码模式非常简单:我们检索我们感兴趣的引用并使用Squish的验证函数之一验证它。当然,如果我们愿意,我们还可以调用对象的方法与之交互。
有关手动编写的测试示例,请参阅手动创建测试、如何创建测试脚本和如何测试应用程序 - 具体内容。
有关验证点的完整覆盖,请参阅如何创建和使用验证点。
测试结果
每次测试运行完成后,测试结果(包括验证点的结果)都会显示在 squishide
下方的“测试结果”视图中。
这是一份详细的测试运行报告,它还包括任何失败或错误等详细信息。如果您单击测试结果条目,squishide
将突出显示生成测试结果的脚本行。如果您展开测试结果条目,您还可以看到测试的更多详细信息。
Squish的测试结果界面非常灵活。通过实现自定义报告生成器,可以以许多不同的方式处理测试结果,例如将它们存储在数据库中,或将它们输出为HTML文件。默认的报告生成器仅在从命令行运行Squish时将结果打印到 stdout
,或在使用 squishide
时将其打印到测试结果视图。您可以通过在“测试结果”上单击并选择“导出结果”菜单选项来将测试结果从 squishide
保存为XML。有关报告生成器的列表,请参阅squishrunner –reportgen: 生成报告。您还可以直接将测试结果记录到数据库中。请参阅如何在Squish测试脚本中访问数据库。
如果您使用 squishrunner 在命令行上运行测试,您还可以以不同的格式导出结果并将其保存到文件中。请参阅处理测试结果和如何使用测试语句部分获取更多信息。
手动创建测试
既然我们已经了解了如何记录测试并通过添加验证点来修改它,我们现在准备了解如何手动创建测试。这样做最简单的方法是修改和重构记录的测试,尽管从头开始也可以完全创建手动测试。
编写手动测试可能最具挑战性的部分是使用正确的对象名称,但实践中这很少是问题。我们可以复制Squish在记录上一个测试时已经添加到对象映射中的符号名称,或者我们可以直接从记录的测试中复制对象名称。如果我们还没有记录任何测试并从头开始,我们可以使用间谍。我们通过单击 启动AUT 工具栏按钮来完成此操作。这启动了AUT并切换到 间谍视角。然后我们可以与AUT交互,直到我们感兴趣的对象可见。然后,在 squishide
中,我们可以导航到应用程序对象视图中的对象,并使用上下文菜单将该对象添加到对象映射中(这样Squish就会记住它),并将其复制到剪贴板(这样我们就可以将其粘贴到我们的测试脚本中)。最后,我们可以单击 退出AUT 工具栏按钮来终止AUT并将Squish返回到 测试管理视角。请参阅如何使用间谍和间谍视角以获取有关使用间谍的更多详细信息。
我们可以通过点击 对象图 工具栏按钮来查看对象图(也可参见,对象图视图)。Squish与之交互的每个应用程序对象都列于此处,无论是顶级对象还是子对象(视图是树形视图)。通过 Ctrl+Click 我们感兴趣的物体,然后点击上下文菜单的“复制”项,我们可以检索Squish在记录脚本中使用的符号名称。这在我们要修改现有测试脚本或要从头创建测试脚本时非常有用,正如我们将在后续教程中看到的那样。
修改和重构已记录的测试
假设我们想要通过添加三个新的名称和地址来测试AUT的“添加”功能。我们当然可以记录这样的测试,但使用代码完成一切也很简单。测试脚本需要执行的操作为:首先点击“文件” > 新建 创建一个新的地址簿,然后为每个新的名称和地址,点击 添加 工具栏按钮,填写详细信息,然后点击 添加。最后, 退出 而不保存。我们还希望在开始时验证没有数据行,在结束时有三行数据。我们还会在过程中进行重构,使代码尽可能整洁和模块化。
首先我们必须创建一个新测试用例。点击 文件 > 新建测试用例 并将测试用例的名称设置为 tst_adding
。Squish将自动创建一个 test.py
(或 test.js
等等)文件。
命令行用户只需在测试套件的目录内创建一个 tst_adding
目录,并在该目录内创建和编辑 test.py
文件(或 test.js
等等)。
我们需要做的第一件事是有一种方式来启动AUT,然后调用菜单选项。以下是记录的 脚本 tst_general
的前几行。
import names import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/mac/addressbook/SquishAddressBook"') activateItem(waitForObject(names.file_NSMenuItem)) activateItem(waitForObject(names.open_NSMenuItem))
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/mac/addressbook/SquishAddressBook"'); activateItem(waitForObject(names.fileNSMenuItem)); activateItem(waitForObject(names.openNSMenuItem));
require 'names.pl'; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/mac/addressbook/SquishAddressBook\""); activateItem(waitForObject($Names::file_nsmenuitem)); activateItem(waitForObject($Names::open_nsmenuitem));
require 'squish' require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/mac/addressbook/SquishAddressBook\"") activateItem(waitForObject(Names::File_NSMenuItem)) activateItem(waitForObject(Names::Open_NSMenuItem))
代码的格式很简单:启动AUT,等待文件菜单出现,然后激活文件菜单;等待菜单项,然后激活菜单项。在这种情况下我们使用了我们已经见过的/a href="squish-api.html#waitforobject-function" translate="no">Object waitForObject(objectOrName) 和activateItem(objectOrName) 函数。
注意: 把我们的函数放在 tst_adding
中可能看起来是个浪费,因为我们也可以在 tst_general
和其他测试用例中使用它们。然而,为了使教程简单,我们将代码放在 tst_adding
测试用例中。有关如何共享脚本,请参阅 如何创建和使用共享数据和共享脚本。
如果您查看记录的测试(tst_general
)或在对象映射中,您会发现 Squish 有时会对相同的事物使用 不同的 名称。例如,窗口以两种不同的方式被识别,最初以 AddressBook_Untitled_NSWindow
的方式,但如果用户单击 文件 > 打开 并打开 MyAddresses.adr
文件,则窗口随后被识别为 AddressBook_MyAddresses_adr_NSWindow
。之所以如此,是因为 Squish 需要在给定的上下文中唯一地识别每个对象,并且它使用它手头的任何信息。因此,在识别主窗口(及其子窗口)时,Squish 使用窗口标题文本为其提供一些上下文。(例如,应用程序的“文件”或“编辑”菜单的选项可能因文件是否加载以及应用程序的状态而异。)
当然,当我们编写测试脚本时,我们不想知道或关心使用哪个特定的名称变体,Squish 通过提供替代命名方案来支持这种需求,我们将在稍后看到。
如果 AUT 在测试执行期间似乎冻结,请等待 Squish 超时 AUT(大约 20 秒),并显示 对象未找到对话框,指示如下的错误
这通常意味着 Squish 在对象映射中没有任何具有给定名称或属性值的对象。从这里,我们可以 选择新对象,调试,抛出错误,或者在选择了新对象之后 重试。
选择新对象将更新符号名称的对象映射条目。除了 对象选择器(《》)之外,我们还可以使用 Spy 的 应用程序对象视图 来定位我们感兴趣的对象,并使用 将到对象映射中 的上下文菜单操作来获取它们的真实或符号名称。
命名很重要,因为这可能是编写脚本中导致最多错误消息的部分,通常是上面显示的 对象 ... 未找到 类型的。一旦我们确定了要访问的测试中的对象,使用 Squish 编写测试脚本就非常简单。尤其是,因为 Squish 很可能支持你最熟悉的脚本语言。
我们现在几乎准备好编写我们自己的测试脚本。通常,最好是从录制一个虚拟测试开始。因此,单击 文件 > 新测试用例 并将测试用例的名称设置为 tst_dummy
。然后点击虚拟测试用例的 录制(《》)。一旦 AUT 启动,单击 文件 > 新,然后单击(空)表格,然后单击 添加 并添加一个条目,然后按 Return 或单击 OK。最后,单击 SquishAddressBook > 退出 Squish 地址簿 完成,并拒绝保存更改。然后仅为了确认一切正常,重新播放此测试。这样做的唯一目的是确保 Squish 将必要的名称添加到对象映射中,因为这样做可能比使用 Spy 为每个感兴趣的对象更快。在重新播放虚拟测试后,如果您想的话可以删除它。
在对象映射中有了所有需要的对象名称后,我们就可以从头开始编写我们的测试脚本。我们将从 main
函数开始,然后我们将查看 main
函数使用的支持函数。
import names import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/mac/addressbook/SquishAddressBook"') table = waitForObject(names.address_Book_Untitled_NSTableView) mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem)) test.verify(table.numberOfRows() == 0) data = [("Andy", "Beach", "[email protected]", "555 123 6786"), ("Candy", "Deane", "[email protected]", "555 234 8765"), ("Ed", "Fernleaf", "[email protected]", "555 876 4654")] for oneNameAndAddress in data: addNameAndAddress(oneNameAndAddress) test.compare(table.numberOfRows(), len(data)) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/mac/addressbook/SquishAddressBook"'); var table = waitForObject(names.addressBookUntitledNSTableView); mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem)); test.verify(table.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(table.numberOfRows(), data.length); closeWithoutSaving(); }
require 'names.pl'; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/mac/addressbook/SquishAddressBook\""); my $table = waitForObject($Names::address_book_untitled_nstableview); mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem)); test::verify($table->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($table->numberOfRows(), scalar(@data)); closeWithoutSaving(); }
require 'squish' require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/mac/addressbook/SquishAddressBook\"") table = waitForObject(Names::Address_Book_Untitled_NSTableView) mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem)) Test.verify(table.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(table.numberOfRows(), data.length) closeWithoutSaving end
我们从调用ApplicationContext startApplication(autName)
函数来启动应用程序开始。我们传递给字符串的名字是在Squish中注册的名字(通常是可执行文件的名字)。然后,我们获取对NSTableView
的引用。我们使用的对象名在tst_general
测试用例记录时并未放入对象映射表中,因此我们记录了一个虚拟测试用例,以确保名字已被添加——当然我们也可以使用Spy。然后,我们将对象映射表中的名字复制到我们的代码中。函数Object waitForObject(objectOrName)
会等待一个对象准备好(可见并启用),然后返回对该对象的引用——或者超时并抛出可捕获的异常。我们使用了一个符号名
来访问表格——这些是Squish记录测试时所使用的名字——而不是一个真的/包含多个属性的名称。最好尽可能使用符号名字,因为在任何AUT对象名字改变时,使用符号名字我们只需要更新对象映射表
,而不需要改变我们的测试代码。一旦我们有了table
引用,我们就可以使用它来访问NSTableView
的任何公共方法和属性。
一旦我们启动了AUT并获取了NSTableView
的引用,我们就告诉Squish点击新工具栏按钮来创建一个新的空地址簿。接下来,我们验证表中的行数是否为0。当我们只想验证一个条件为真而不是比较两个不同的值时,Boolean test.verify(condition)
函数非常有用。(对于Tcl,我们通常使用Boolean test.compare(value1, value2)
函数而不是Boolean test.verify(condition)
函数,因为它的使用稍微简单一些。)
接下来,我们创建一些示例数据,并调用自定义的addNameAndAddress()
函数,使用AUT的“添加表单”填充表格中的数据。然后我们再次比较表中的行数,这一次是和我们的示例数据中的行数进行比较。最后,我们调用自定义的closeWithoutSaving
函数来关闭应用程序。
现在,我们将回顾这两个辅助函数,以便涵盖tst_adding
测试用例中的所有代码,首先从addNameAndAddress()
开始。
def addNameAndAddress(oneNameAndAddress): mouseClick(waitForObject(names.address_Book_Untitled_Add_NSToolbarItem)) type(waitForObject(names.add_Address_NSTextField), oneNameAndAddress[0]) type(waitForObject(names.add_Address_NSTextField_2), oneNameAndAddress[1]) type(waitForObject(names.add_Address_NSTextField_3), oneNameAndAddress[2]) type(waitForObject(names.add_Address_NSTextField_4), oneNameAndAddress[3]) mouseClick(waitForObject(names.add_Address_Add_NSButton));
function addNameAndAddress(oneNameAndAddress) { mouseClick(waitForObject(names.addressBookUntitledAddNSToolbarItem)); type(waitForObject(names.addAddressNSTextField), oneNameAndAddress[0]); type(waitForObject(names.addAddressNSTextField_2), oneNameAndAddress[1]); type(waitForObject(names.addAddressNSTextField_3), oneNameAndAddress[2]); type(waitForObject(names.addAddressNSTextField_4), oneNameAndAddress[3]); mouseClick(waitForObject(names.addAddressAddNSButton)); }
sub addNameAndAddress { my (@oneNameAndAddress) = @_; mouseClick(waitForObject($Names::address_book_untitled_add_nstoolbaritem)); type(waitForObject($Names::add_address_nstextfield), $oneNameAndAddress[0]); type(waitForObject($Names::add_address_nstextfield_2), $oneNameAndAddress[1]); type(waitForObject($Names::add_address_nstextfield_3), $oneNameAndAddress[2]); type(waitForObject($Names::add_address_nstextfield_4), $oneNameAndAddress[3]); mouseClick(waitForObject($Names::add_address_add_nsbutton)); }
def addNameAndAddress(oneNameAndAddress) mouseClick(waitForObject(Names::Address_Book_Untitled_Add_NSToolbarItem)) type(waitForObject(Names::Add_Address_NSTextField), oneNameAndAddress[0]) type(waitForObject(Names::Add_Address_NSTextField_2), oneNameAndAddress[1]) type(waitForObject(Names::Add_Address_NSTextField_3), oneNameAndAddress[2]) type(waitForObject(Names::Add_Address_NSTextField_4), oneNameAndAddress[3]) mouseClick(waitForObject(Names::Add_Address_Add_NSButton)) end
对于每一组名字和地址数据,我们点击添加以弹出添加表单。然后,对于每个收到的值,我们通过等待相关的NSTextField
准备好并使用type(objectOrName, text)
函数输入文本,来填充相应的字段。最后,我们点击表单上的添加按钮。我们通过从记录的tst_general
测试用例复制并简单地对字段名和文本进行参数化得到了函数的核心代码。同样地,我们从tst_general
测试用例的代码中复制了点击添加按钮的代码。
def closeWithoutSaving(): type(waitForObject(names.address_Book_Untitled_NSTableView), "<Command+q>") mouseClick(waitForObject(names.save_Changes_Don_t_Save_NSButton))
function closeWithoutSaving() { type(waitForObject(names.addressBookUntitledNSTableView), "<Command+q>"); mouseClick(waitForObject(names.saveChangesDonTSaveNSButton)); }
sub closeWithoutSaving { type(waitForObject($Names::address_book_untitled_nstableview), "<Command+q>"); mouseClick(waitForObject($Names::save_changes_don_t_save_nsbutton)); }
def closeWithoutSaving type(waitForObject(Names::Address_Book_Untitled_NSTableView), "<Command+q>") mouseClick(waitForObject(Names::Save_Changes_Don_t_Save_NSButton)) end
在这里,我们通过按Command+Q来退出应用程序,然后点击“保存更改”对话框的“不保存”按钮。最后一行是从记录的测试中复制的。
整个测试大约有25行代码——如果我们把一些公用函数(比如closeWithoutSaving]
)放在一个共享脚本中,它会更少。代码的大部分都直接从记录的测试中复制,并且某些情况下进行了参数化。
这应该足以让人们了解如何为AUT编写测试脚本。请注意,Squish提供的功能远不止我们在这里使用的这些。(所有这些都在API参考手册和工具参考手册中有涵盖。)Squish还提供了访问AUT对象全部公共API的能力。
然而,测试用例的一个方面并不令人非常满意。虽然像我们这里这样嵌入测试数据是有意义的,但是对于小型数据量来说,它相当有限,尤其是当我们想使用大量测试数据时。此外,我们没有测试任何新添加的数据,以查看它们是否正确地出现在NSTableView
中。在下文部分中,我们将创建这个测试的新版本,但这次将从外部数据源获取数据,并检查添加到NSTableView
中的数据是否正确。
创建数据驱动测试
在前一部分中,我们在测试中放入了三个硬编码的名称和地址。但如果我们想测试很多数据怎么办?或者如果我们想在不需要更改测试脚本的源代码的情况下更改数据怎么办?一个方法是导入数据集到Squish中,并将数据集作为插入到测试中的值源。Squish可以导入.tsv
(制表符分隔值格式)、.csv
(逗号分隔值格式)、.xls
或.xlsx
(Microsoft Excel电子表格格式)数据。
注意: .csv
和.tsv
文件都假设使用Unicode UTF-8编码—与所有测试脚本相同的编码。
测试数据可以使用squishide
导入,或者手动使用文件管理器或控制台命令导入。我们将描述这两种方法,从使用squishide
开始。
对于SquishAddressBook应用程序,我们想导入MyAddresses.tsv
数据文件。为此,我们必须单击文件
> 导入测试资源
以弹出导入Squish资源
对话框。在对话框中,单击浏览
按钮来选择要导入的文件—在本例中为MyAddresses.tsv
。确保将导入为
组合框设置为“测试数据”。默认情况下,squishide
仅导入当前测试用例的测试数据,但我们希望测试数据对所有测试套件的测试用例都可用:为此,请选中“Copy to Test Suite for Sharing
”的单选按钮。现在单击完成
按钮。你现在可以在测试套件资源视图(在测试数据选项卡中)中看到文件列表,并且如果您点击文件名,它将显示在编辑器视图
中。截图显示了添加测试数据后的Squish。
要从squishide
外部导入测试数据,请使用文件管理器,例如文件资源管理器或Finder,或使用控制台命令。在测试套件的目录内创建一个名为shared
的目录。然后,在shared
目录内创建一个名为testdata
的目录。将数据文件(在本例中为MyAddresses.tsv
)复制到shared\testdata
目录中。
如果squishide
正在运行,请重新启动它。如果您单击测试套件资源视图的“测试数据”选项卡,您应该能看到数据文件。单击文件名,以在编辑器视图
中查看该文件。
尽管在实际生活中,我们会修改我们的tst_adding
测试案例,使用测试数据,但为了教程的目的,我们将创建一个新的测试案例,名为tst_adding_data
,它是tst_adding
的副本,我们将修改它以使用测试数据。
我们要更改的唯一函数是main
,而不是遍历硬编码的数据项,我们将遍历数据集中的所有记录。我们还需要更新预期的记录数,因为我们现在添加的记录要多得多,我们还将添加一个函数来验证每条添加的记录。
import names import os def main(): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/mac/addressbook/SquishAddressBook"') table = waitForObject(names.address_Book_Untitled_NSTableView) mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem)) test.verify(table.numberOfRows() == 0) limit = 10 # To avoid testing 100s of rows 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 a single tuple checkNameAndAddress(row, table, record) if row > limit: break test.compare(table.numberOfRows(), row + 1) closeWithoutSaving()
import * as names from 'names.js'; function main() { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/mac/addressbook/SquishAddressBook"'); var table = waitForObject(names.addressBookUntitledNSTableView); mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem)); test.verify(table.numberOfRows() == 0); var limit = 10; // To avoid testing 100s of rows since that would be boring var records = testData.dataset("MyAddresses.tsv"); for (var row = 0; row < records.length; ++row) { var record = records[row]; var forename = testData.field(record, "Forename"); var surname = testData.field(record, "Surname"); var email = testData.field(record, "Email"); var phone = testData.field(record, "Phone"); addNameAndAddress(new Array(forename, surname, email, phone)); checkNameAndAddress(row, table, record); if (row > limit) break; } test.compare(table.numberOfRows(), row + 1); closeWithoutSaving(); }
require 'names.pl'; sub main { startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/mac/addressbook/SquishAddressBook\""); my $table = waitForObject($Names::address_book_untitled_nstableview); mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem)); test::verify($table->numberOfRows() == 0); my $limit = 10; # To avoid testing 100s of rows since that would be boring my @records = testData::dataset("MyAddresses.tsv"); my $row = 0; for (; $row < scalar(@records); ++$row) { my $record = $records[$row]; my $forename = testData::field($record, "Forename"); my $surname = testData::field($record, "Surname"); my $email = testData::field($record, "Email"); my $phone = testData::field($record, "Phone"); addNameAndAddress($forename, $surname, $email, $phone); checkNameAndAddress($row, $table, $record); if ($row > $limit) { last; } } test::compare($table->numberOfRows(), $row + 1); closeWithoutSaving(); }
require 'squish' require 'names' include Squish def main startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/mac/addressbook/SquishAddressBook\"") table = waitForObject(Names::Address_Book_Untitled_NSTableView) mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem)) Test.verify(table.numberOfRows() == 0) limit = 10 # To avoid testing 100s of rows 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(row, table, record) break if row > limit rows += 1 end Test.verify(table.numberOfRows() == rows + 1) closeWithoutSaving end
Squish通过其testData
模块的功能提供对测试数据的访问——在这里,我们使用了Dataset testData.dataset(filename)函数来访问数据文件并使其记录可用,以及String testData.field(record, fieldName)函数来检索每个记录的各个字段。
使用测试数据填充NSTableView
后,我们希望确信表中的数据与我们添加的数据相同,因此我们添加了checkNameAndAddress
函数。我们还对要比较的记录数量进行了限制,以确保测试运行更快。
def checkNameAndAddress(row, table, record): for column in range(len(testData.fieldNames(record))): cellName = {'rowNumber': row, 'columnNumber': column, 'tableView': names.address_Book_Untitled_NSTableView, 'type': 'NSString'} cell = waitForObject(cellName) test.compare(cell.stringValue, testData.field(record, column))
function checkNameAndAddress(row, table, record) { for (var column = 0; column < testData.fieldNames(record).length; ++column) { var cellName = {'rowNumber': row, 'columnNumber': column, 'tableView': names.addressBookUntitledNSTableView, 'type': 'NSString'}; var cell = waitForObject(cellName); test.compare(cell.stringValue, testData.field(record, column)); } }
def checkNameAndAddress(row, table, record) for column in 0...TestData.fieldNames(record).length cellName = {:rowNumber => row, :columnNumber => column, :tableView => Names::Address_Book_Untitled_NSTableView, :type => 'NSString'} cell = waitForObject(cellName) Test.compare(cell.stringValue, TestData.field(record, column)) end end
该函数访问给定的NSTableView
行并提取其每个列的值。我们使用Squish的SequenceOfStrings testData.fieldNames(record)函数获取列数,然后使用Boolean test.compare(value1, value2)函数检查表中每个值是否与我们使用的测试数据中的值相同。
如我们之前提到的,Squish用于菜单、菜单项(以及其他对象)的符号名称可能会根据上下文而变化,通常名称的开始由窗口标题派生。对于将当前文件名放入标题的应用程序(如地址簿示例),名称将包含文件名,我们必须考虑到这一点。
在地址簿示例的情况下,主窗口的标题是"地址簿"(启动时),或"地址簿 - 无标题"(在文件 > 新建后,但在文件 > 保存或文件 > 另存为之前),或"地址簿 - filename",其中filename可以变化。我们的代码通过使用真实(多属性)名称来处理所有这些情况。
符号名称嵌入有关对象及其类型的信息片段。真实名称由花括号包围的空格分隔的关键字值对列表表示。每个真实名称都必须指定type
属性以及至少其他一个属性。在这里,我们使用了rowNumber
、columnNumber
、tableView
和type
属性来唯一标识表中的每个单元格,以便我们可以比较每个单元格的stringValue与填充它所使用的数据。
截图显示了运行数据驱动的测试后Squish的测试摘要日志。
Squish还可以做关键词驱动的测试。这比数据驱动的测试更复杂。请参阅如何进行关键词驱动测试。
了解更多
现在我们已经完成了教程。Squish可以做到我们在这里展示的很多更多,但目标是最快、最简单地让你开始基本测试。有关如何创建测试脚本(如何创建测试脚本)和如何测试应用程序-具体实践的章节提供了更多的示例,包括显示测试如何与特定的输入元素交互的示例,例如选择框、选择单一、文本和文本区域。
API参考和工具参考提供了关于Squish测试API的完整细节以及它提供的许多函数,以便尽可能使测试变得简单和高效。阅读如何创建测试脚本和如何测试应用程序-具体实践,以及浏览API参考和工具参考是非常值得的。你投入的时间将得到回报,因为你会知道Squish提供了哪些开箱即用的功能,并且可以避免重新发明已经可用的东西。
如果您对测试iPhone应用感兴趣,请参阅教程:开始测试iOS应用。
教程:设计行为驱动开发(BDD)测试
本教程将向您展示如何为一个示例应用创建、运行和修改行为驱动开发(BDD)测试。您将了解Squish最常用的功能。到教程结束时,您将能够为您自己的应用程序编写测试。
在本章中,我们将使用一个简单的地址簿应用作为我们的应用测试(AUT)。在您的examples/addressbook
文件夹中,它被称为SquishAddressBook.app。这是一个非常基础的应用,允许用户加载数据库中的地址簿或创建一个新地址簿,添加、编辑和删除条目。截图显示了新建的空地址簿。
行为驱动开发简介
行为驱动开发(BDD)是测试驱动开发(TDD)的扩展,它将验收标准的定义放在开发过程的开始阶段,而不是在软件开发完成后编写测试。这个过程还允许在测试后进行代码变更的可能周期。
行为驱动测试由一系列特性
文件构成,通过一个或多个场景
描述产品特性预期应用行为。每个场景
由一系列步骤组成,这些步骤表示需要对该场景
进行测试的操作或验证。
BDD关注预期应用行为,而非实现详细信息。因此,BDD测试使用人类可读的领域特定语言(DSL)进行描述。由于这种语言非技术性,不仅程序员可以创建这类测试,产品负责人、测试员或业务分析师也可以。此外,在产品开发过程中,此类测试可作为活的产品文档。对于Squish的使用,BDD测试应使用Gherkin语法。以前编写的产品规范(BDD测试)可以转换为可执行测试。本分步教程展示了使用squishide
支持来自动化BDD测试。
Gherkin语法
Gherkin文件通过一个或多个场景描述预期应用行为来描述产品特性。以下是一个演示地址簿示例应用程序中的“填充地址簿”功能的示例。
Feature: Filling of the address book As a user I want to fill the address book with entries Scenario: Initial state of the newly created address book Given the address book application is running When I create a new address book Then the address book should be empty Scenario: State after adding one entry Given the address book application is running When I create a new address book And I add a new person 'John','Doe','[email protected]','500600700' to the address book Then '1' entries should be present Scenario: State after adding two entries Given the address book application is running When I create a new address book And I add new persons to the address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present Scenario: Forename and surname is added to table Given the address book application is running When I create a new address book And I add a new person 'Bob','Doe','[email protected]','123321231' to the address book Then previously entered forename and surname shall be at the top
上面的内容大多是自由格式文本(不需要英语)。只是固定了特性
/场景
结构,以及诸如给定
、并且
、时候
和那么
之类的关键字。这些关键字标记的步骤定义了前提条件、用户操作和预期结果。上述应用程序行为描述可以传递给软件开发人员以实现功能,同时也可以传递给软件开发人员进行自动化测试。
测试实现
创建测试套件
首先,我们需要创建一个测试套件,它是一个所有测试用例的容器。启动SquishIDE,选择文件 > 新建测试套件。按照新建测试套件向导提供测试套件名称,选择Mac工具包和所选脚本语言,最后将SquishAddressBook
应用程序注册为AUT。请参阅创建测试套件以获取有关创建新测试套件的更多详细信息。
创建测试用例
Squish提供两种类型的测试用例:“脚本测试用例”和“BDD测试用例”。由于“脚本测试用例”是默认选项,为了创建新的BDD测试用例,我们需要通过点击新建脚本测试用例旁边的扩展器()并选择新建BDD测试用例来使用下拉菜单。SquishIDE将记住您的选择,并且在将来单击按钮时,“BDD测试用例”将变为默认选项。
新创建的BDD测试用例包括一个test.feature
文件(在创建新的BDD测试用例时填充了Gherkin模板),一个名为test.(py|js|pl|rb|tcl)
的文件,它将驱动执行(无需编辑此文件),以及一个名为shared/steps/steps.(py|js|pl|rb|tcl)
的测试套件资源文件,其中将放置步骤实现代码。
我们需要将Gherkin模板替换为示例应用程序的Feature
。为此,复制下面的Feature
描述并将其粘贴到Feature
文件中。
Feature: Filling of the address book
As a user I want to fill the address book with entries
Scenario: Initial state of the newly created address book
Given the address book application is running
When I create a new address book
Then the address book should be empty
在编辑test.feature
文件时,对于每个未定义的步骤,将显示一个Feature
文件警告“未找到实现”。实现代码位于steps
子目录中,在测试用例资源或测试套件资源中。现在运行我们的Feature
测试将会在第一步失败,显示“No Matching Step Definition”,后面的步骤将被跳过。
录制步骤实现
为了录制Scenario
,请按下位于测试用例资源视图中Scenarios选项卡中相应Scenario
旁边的记录()。
这将导致Squish运行AUT,这样我们就可以与之交互。此外,将显示带有所有需要录制的步骤的控制栏。现在,所有与AUT的交互或添加到脚本中的任何验证点都将记录在当前录制的步骤下,Given the address book application is running
(在控制栏的步骤列表中加粗)。为了验证这个前提条件是否满足,我们将添加一个验证点。为此,请单击控制栏中的验证按钮,然后选择属性。
结果,squishide
进入Spy模式,在可拖拽的视图中显示所有应用程序对象及其属性。在应用程序对象视图中,选择主窗口项(Address Book - Untitled_NSWindow
)。选择它将更新右侧的属性视图。接下来,单击属性视图中的isVisible
属性前面的复选框。最后,单击保存并插入验证按钮。squishide
消失,并且控制栏再次显示。
完成每个步骤后,我们可以通过单击控制栏中位于当前步骤左侧的完成录制步骤(
接下来,对于步骤 当创建一个新地址簿
,点击地址簿应用程序工具栏中的新建按钮(完成录制步骤(
最后,对于步骤 地址簿应为空
,检查包含地址条目的表是否为空。要在录制过程中记录此验证,请在录制时单击验证并选择属性。现在,在应用程序对象中,导航或使用对象选择器(Table对象不提供行数属性。这些信息是直接不可用的。因此,我们需要稍后修改录制的代码。为了让 Squish 为我们的步骤生成一些test.compare()
代码,请插入对随机属性的验证,例如对齐。最后,单击控制栏中的最后一个完成录制步骤(
因此,Squish 将在steps.*
文件中生成以下步骤定义(在测试用例资源视图中步骤选项卡)
@Given("the address book application is running") def step(context): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/mac/addressbook/SquishAddressBook"') test.compare(waitForObjectExists(names.o_NSWindow).isVisible, 0) @When("I create a new address book") def step(context): mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem)) @Then("the address book should be empty") def step(context): #test.compare(waitForObjectExists(names.address_Book_Untitled_NSTableView).alignment, 0) test.compare(waitForObjectExists(names.address_Book_Untitled_NSTableView).numberOfRows(), 0)
Given("addressbook application is running", function(context) { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/mac/addressbook/SquishAddressBook"'); test.compare(waitForObjectExists(names.addressBookUntitledNSWindow).isVisible, 1); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem)); }); Then("addressbook should have zero entries", function(context) { //test.compare((waitForObject(names.addressBookUntitledNSTableView)).alignment, 0); test.compare((waitForObject(names.addressBookUntitledNSTableView)).numberOfRows(), 0); });
Given("the address book application is running", sub { my $context = shift; startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/mac/addressbook/SquishAddressBook\""); test::compare(waitForObjectExists($Names::address_book_untitled_nswindow)->isVisible, 1); }); When("I create a new address book", sub { my $context = shift; mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem)); }); Then("the address book should be empty", sub { my $context = shift; #test::compare(waitForObjectExists($Names::address_book_untitled_nstableview)->alignment, 1); test::compare(waitForObjectExists($Names::address_book_untitled_nstableview)->numberOfRows, 0); });
Given("the address book application is running") do |context| startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/mac/addressbook/SquishAddressBook\"") Test.compare(waitForObjectExists(Names::O_NSWindow).isVisible, 0) end When("I create a new address book") do |context| mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem)) end Then("the address book should be empty") do |context| #Test.compare(waitForObjectExists(Names::Address_Book_Untitled_NSTableView).alignment, 0) Test.compare(waitForObjectExists(Names::Address_Book_Untitled_NSTableView).numberOfRows(), 0) end
为了获取行数,我们必须调用底层的 NSTableView
对象的某个方法。
由于录制的 startApplication()
调用,应用程序在第一步的开始时自动启动。在每个场景结束时,将调用 OnScenarioEnd
钩子,导致在应用程序上下文中调用 detach()
,因为 AUT 是通过 startApplication()
启动的,这会导致它终止。此钩子函数在测试用例资源视图的脚本选项卡中找到的 bdd_hooks.(py|js|pl|rb|tcl)
文件中。您可以在那里定义额外的钩子函数。有关所有可用钩子的列表,请参阅通过钩子执行测试执行期间的操作。
@OnScenarioEnd def OnScenarioEnd(): for ctx in applicationContextList(): ctx.detach()
OnScenarioEnd(function(context) { applicationContextList().forEach(function(ctx) { ctx.detach(); }); });
OnScenarioEnd(sub { foreach (applicationContextList()) { $_->detach(); } });
OnScenarioEnd do |context| applicationContextList().each { |ctx| ctx.detach() } end
步骤参数化
到目前为止,我们的步骤没有使用任何参数,所有值都是硬编码的。Squish 提供不同类型的参数,如 any
、integer
或 word
,使得我们的步骤定义更具可重用性。让我们给我们的 Feature
文件添加一个新 Scenario
,它将为测试数据和预期结果提供步骤参数。将以下部分复制到您的特征文件中。
Scenario: State after adding one entry Given the address book application is running When I create a new address book And I add a new person 'John','Doe','[email protected]','500600700' to the address book Then '1' entries should be present
Feature
文件自动保存后,squishide
提供了一条提示,只需实现两个步骤:当我向地址簿添加新人物 'John', 'Doe','[email protected]','500600700'
和然后应该有 '1' 条条目
。其余步骤已具有相应的实现。
要记录缺失的步骤,请点击 录制 (),它在“测试套件”视图中测试用例名称旁边的。脚本会一直播放直到到达缺失步骤,然后提示您实现它。如果您选择“添加”按钮,则可以输入新条目的信息。点击“完成录制步骤”()以进入下一步骤。对于第二个缺失步骤,我们可以像处理步骤一样记录一个对象属性验证,例如:地址簿应该为空
。或者,我们可以将步骤的实现复制到steps.(py|js|pl|rb|tcl)
文件,并增加test.compare()
行末的数字。而不是测试零项,我们将测试一项。
我们现在通过对参数类型进行替换来参数化生成的步骤实现。由于我们想能够添加不同的名称,将John
替换为'|word|'
。请注意,每个参数将按照步骤名称描述中出现的顺序传递给步骤实现函数。通过编辑输入的值到关键字来完成参数化,就像这个示例步骤一样:当我向地址簿添加一个新的人 'John', 'Doe', '[email protected]', '500600700'
@When("I add a new person '|word|','|word|','|any|','|integer|' to the address book") def step(context, forename, surname, email, phone): mouseClick(waitForObject(names.address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(names.add_Address_NSTextField), 42.6172, 5.125, 0, 0) type(waitForObject(names.add_Address_NSTextField), forename) mouseClick(waitForObject(names.add_Address_NSTextField_2), 95.7852, 14.4336, 0, 0) type(waitForObject(names.add_Address_NSTextField_2), surname) mouseClick(waitForObject(names.add_Address_NSTextField_3), 93.7422, 9.19531, 0, 0) type(waitForObject(names.add_Address_NSTextField_3), email) mouseClick(waitForObject(names.add_Address_NSTextField_4), 98.6094, 15.3789, 0, 0) type(waitForObject(names.add_Address_NSTextField_4), phone) mouseClick(waitForObject(names.add_Address_Add_NSButton)) context.userData = {'forename': forename, 'surname': surname}
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) { mouseClick(waitForObject(names.addressBookUntitledAddNSToolbarItem)); mouseClick(waitForObject(names.addAddressNSTextField), 54.3047, 9.10156, 0, 0); type(waitForObject(names.addAddressNSTextField), forename); mouseClick(waitForObject(names.addAddressNSTextField_2), 45.3555, 10.9453, 0, 0); type(waitForObject(names.addAddressNSTextField_2), surname); mouseClick(waitForObject(names.addAddressNSTextField_3), 49.4805, 8.01562, 0, 0); type(waitForObject(names.addAddressNSTextField_3), email); mouseClick(waitForObject(names.addAddressNSTextField_4), 46.3125, 12.1641, 0, 0); type(waitForObject(names.addAddressNSTextField_4), phone); mouseClick(waitForObject(names.addAddressAddNSButton)); context.userData["forename"] = forename; context.userData["surname"] = surname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to the address book", sub { my $context = shift; my $forename = shift; my $surname = shift; my $email = shift; my $phone = shift; mouseClick( waitForObject($Names::address_book_untitled_add_nstoolbaritem) ); type( waitForObject($Names::add_address_nstextfield), $forename ); mouseClick( waitForObject($Names::add_address_nstextfield_2), 62.4609, 14.2227, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_2), $surname ); mouseClick( waitForObject($Names::add_address_nstextfield_3), 60.375, 15.5, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_3), $email ); mouseClick( waitForObject($Names::add_address_nstextfield_4), 108.117, 15.7734, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_4), $phone ); mouseClick( waitForObject($Names::add_address_add_nsbutton) ); $context->{userData}{'forename'} = $forename; $context->{userData}{'surname'} = $surname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to the address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::Address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(Names::Add_Address_NSTextField), 57.1602, 4.91016, 0, 0) type(waitForObject(Names::Add_Address_NSTextField), forename) mouseClick(waitForObject(Names::Add_Address_NSTextField_2), 71.3672, 17.5703, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_2), surname) mouseClick(waitForObject(Names::Add_Address_NSTextField_3), 71.3672, 16.5, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_3), email) mouseClick(waitForObject(Names::Add_Address_NSTextField_4), 79.707, 16.7734, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_4), phone) mouseClick(waitForObject(Names::Add_Address_Add_NSButton)) context.userData = Hash.new context.userData[:forename] = forename context.userData[:surname] = surname end
在表中为步骤提供参数
下一个场景
将测试添加到地址簿中的多个条目。我们可以使用步骤当我向地址簿添加一个新的人员 John','Doe','[email protected]','500600700'
多次,只需更改数据即可。但让我们定义一个新的步骤,称为并且向地址簿添加新的人员
,该步骤将处理来自表格的数据。
Scenario: State after adding two entries Given the address book application is running When I create a new address book And I add new persons to the address book | forename | surname | email | phone | | John | Smith | john@m.com | 123123 | | Alice | Thomson | alice@m.com | 234234 | Then '2' entries should be present
处理此类表格的步骤实现如下:
@When("I add new persons to the address book") def step(context): table = context.table # Drop initial row with column headers for row in table[1:]: forename = row[0] surname = row[1] email = row[2] phone = row[3] mouseClick(waitForObject(names.address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(names.add_Address_NSTextField), 42.6172, 5.125, 0, 0) type(waitForObject(names.add_Address_NSTextField), forename) mouseClick(waitForObject(names.add_Address_NSTextField_2), 95.7852, 14.4336, 0, 0) type(waitForObject(names.add_Address_NSTextField_2), surname) mouseClick(waitForObject(names.add_Address_NSTextField_3), 93.7422, 9.19531, 0, 0) type(waitForObject(names.add_Address_NSTextField_3), email) mouseClick(waitForObject(names.add_Address_NSTextField_4), 98.6094, 15.3789, 0, 0) type(waitForObject(names.add_Address_NSTextField_4), phone) mouseClick(waitForObject(names.add_Address_Add_NSButton))
When("I add new persons to address book", function(context) { var table = context.table; // Drop initial row with column headers for (var i = 1; i < table.length; ++i) { var forename = table[i][0]; var surname = table[i][1]; var email = table[i][2]; var phone = table[i][3]; mouseClick(waitForObject(names.addressBookUntitledAddNSToolbarItem)); mouseClick(waitForObject(names.addAddressNSTextField), 54.3047, 9.10156, 0, 0); type(waitForObject(names.addAddressNSTextField), forename); mouseClick(waitForObject(names.addAddressNSTextField_2), 45.3555, 10.9453, 0, 0); type(waitForObject(names.addAddressNSTextField_2), surname); mouseClick(waitForObject(names.addAddressNSTextField_3), 49.4805, 8.01562, 0, 0); type(waitForObject(names.addAddressNSTextField_3), email); mouseClick(waitForObject(names.addAddressNSTextField_4), 46.3125, 12.1641, 0, 0); type(waitForObject(names.addAddressNSTextField_4), phone); mouseClick(waitForObject(names.addAddressAddNSButton)); } });
When("I add new persons to the 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}; mouseClick( waitForObject($Names::address_book_untitled_add_nstoolbaritem) ); type( waitForObject($Names::add_address_nstextfield), $forename ); mouseClick( waitForObject($Names::add_address_nstextfield_2), 62.4609, 14.2227, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_2), $surname ); mouseClick( waitForObject($Names::add_address_nstextfield_3), 60.375, 15.5, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_3), $email ); mouseClick( waitForObject($Names::add_address_nstextfield_4), 108.117, 15.7734, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_4), $phone ); mouseClick( waitForObject($Names::add_address_add_nsbutton) ); } });
When("I add new persons to the address book") do |context| table = context.table # Drop initial row with column headers table.shift for forename,surname,email,phone in table do mouseClick(waitForObject(Names::Address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(Names::Add_Address_NSTextField), 57.1602, 4.91016, 0, 0) type(waitForObject(Names::Add_Address_NSTextField), forename) mouseClick(waitForObject(Names::Add_Address_NSTextField_2), 71.3672, 17.5703, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_2), surname) mouseClick(waitForObject(Names::Add_Address_NSTextField_3), 71.3672, 16.5, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_3), email) mouseClick(waitForObject(Names::Add_Address_NSTextField_4), 79.707, 16.7734, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_4), phone) mouseClick(waitForObject(Names::Add_Address_Add_NSButton)) end end
步骤和场景之间的数据共享
让我们向特性
文件添加一个新的场景
。这次我们想检查的不仅仅是地址簿列表中的条目数量,而是这个列表是否包含正确的数据。因为我们在一个步骤中输入数据并在另一个步骤中进行验证,我们必须以某种方式在这两个步骤之间共享输入数据的信息,以便执行验证。
Scenario: Forename and surname is added to table Given the address book application is running When I create a new address book And I add a new person 'Bob','Doe','[email protected]','123321231' to the 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 the address book") def step(context, forename, surname, email, phone): mouseClick(waitForObject(names.address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(names.add_Address_NSTextField), 42.6172, 5.125, 0, 0) type(waitForObject(names.add_Address_NSTextField), forename) mouseClick(waitForObject(names.add_Address_NSTextField_2), 95.7852, 14.4336, 0, 0) type(waitForObject(names.add_Address_NSTextField_2), surname) mouseClick(waitForObject(names.add_Address_NSTextField_3), 93.7422, 9.19531, 0, 0) type(waitForObject(names.add_Address_NSTextField_3), email) mouseClick(waitForObject(names.add_Address_NSTextField_4), 98.6094, 15.3789, 0, 0) type(waitForObject(names.add_Address_NSTextField_4), phone) mouseClick(waitForObject(names.add_Address_Add_NSButton)) context.userData = {'forename': forename, 'surname': surname}
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) { mouseClick(waitForObject(names.addressBookUntitledAddNSToolbarItem)); mouseClick(waitForObject(names.addAddressNSTextField), 54.3047, 9.10156, 0, 0); type(waitForObject(names.addAddressNSTextField), forename); mouseClick(waitForObject(names.addAddressNSTextField_2), 45.3555, 10.9453, 0, 0); type(waitForObject(names.addAddressNSTextField_2), surname); mouseClick(waitForObject(names.addAddressNSTextField_3), 49.4805, 8.01562, 0, 0); type(waitForObject(names.addAddressNSTextField_3), email); mouseClick(waitForObject(names.addAddressNSTextField_4), 46.3125, 12.1641, 0, 0); type(waitForObject(names.addAddressNSTextField_4), phone); mouseClick(waitForObject(names.addAddressAddNSButton)); context.userData["forename"] = forename; context.userData["surname"] = surname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to the address book", sub { my $context = shift; my $forename = shift; my $surname = shift; my $email = shift; my $phone = shift; mouseClick( waitForObject($Names::address_book_untitled_add_nstoolbaritem) ); type( waitForObject($Names::add_address_nstextfield), $forename ); mouseClick( waitForObject($Names::add_address_nstextfield_2), 62.4609, 14.2227, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_2), $surname ); mouseClick( waitForObject($Names::add_address_nstextfield_3), 60.375, 15.5, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_3), $email ); mouseClick( waitForObject($Names::add_address_nstextfield_4), 108.117, 15.7734, 0, 0 ); type( waitForObject($Names::add_address_nstextfield_4), $phone ); mouseClick( waitForObject($Names::add_address_add_nsbutton) ); $context->{userData}{'forename'} = $forename; $context->{userData}{'surname'} = $surname; });
When("I add a new person '|word|','|word|','|any|','|integer|' to the address book") do |context, forename, surname, email, phone| mouseClick(waitForObject(Names::Address_Book_Untitled_Add_NSToolbarItem)) mouseClick(waitForObject(Names::Add_Address_NSTextField), 57.1602, 4.91016, 0, 0) type(waitForObject(Names::Add_Address_NSTextField), forename) mouseClick(waitForObject(Names::Add_Address_NSTextField_2), 71.3672, 17.5703, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_2), surname) mouseClick(waitForObject(Names::Add_Address_NSTextField_3), 71.3672, 16.5, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_3), email) mouseClick(waitForObject(Names::Add_Address_NSTextField_4), 79.707, 16.7734, 0, 0) type(waitForObject(Names::Add_Address_NSTextField_4), phone) mouseClick(waitForObject(Names::Add_Address_Add_NSButton)) context.userData = Hash.new context.userData[:forename] = forename context.userData[:surname] = surname end
所有存储在context.userData中的数据都可以从给定特性
的所有场景
中的步骤实现和钩子
中访问。最后,我们需要实现步骤然后之前输入的姓和名应位于顶部
。
@Then("previously entered forename and surname shall be at the top") def step(context): table = waitForObject(names.address_Book_Untitled_NSTableView) children = object.children(table) forename = children[0] surname = children[1] test.compare(forename.stringValue, context.userData["forename"]) test.compare(surname.stringValue, context.userData["surname"])
Then("previously entered forename and surname shall be at the top", function(context) { test.compare(object.children(waitForObjectExists(names.addressBookUntitledNSTableView))[0], "Bob"); test.compare(object.children(waitForObjectExists(names.addressBookUntitledNSTableView))[1], "Doe"); });
Then("previously entered forename and surname shall be at the top", sub { my $context = shift; my $table = waitForObject($Names::address_book_untitled_nstableview); my @children = object::children($table); my $forename = $children[0]; my $surname = $children[1]; test::compare( $forename->stringValue, $context->{userData}{'forename'} ); test::compare( $surname->stringValue, $context->{userData}{'surname'} ); });
Then("previously entered forename and surname shall be at the top") do |context| table = waitForObject(Names::Address_Book_Untitled_NSTableView) children = Squish::Object.children(table) forename = children[0] surname = children[1] Test.compare(forename.stringValue, context.userData[:forename]) Test.compare(surname.stringValue, context.userData[:surname]) end
如果您从此处复制了步骤实现或自己编写了它而没有录制,则可能会在重新播放时发生错误:未找到对象 ':Bob_NSString'。对象名称未在对象映射中找到。
这是因为我们引入了新的GUI对象,而没有通过Squish首先访问它们,这会将它们添加到对象映射中。要解决此问题,请按错误对话框中的选择新对象
按钮,然后单击缺少的对象。现在选择重试
以继续重新播放。
场景概述
假设我们的特性
包含以下两个场景
:
Scenario: State after adding one entry Given the address book application is running When I create a new address book And I add a new person 'John','Doe','[email protected]','500600700' to the address book Then "1" entries should be present Scenario: State after adding one entry Given the address book application is running When I create a new address book And I add a new person 'Bob','Koo','[email protected]','500600800' to the address book Then "1" entries should be present
如我们所见,这些场景
使用不同的测试数据执行相同的操作。同样可以通过使用场景概述
(一个带有占位符的场景模板)和示例
(一个带有参数的表格)来实现。
Scenario Outline: Adding single entries multiple times Given the address book application is running When I create a new address book And I add a new person '<forename>','<surname>','<email>','<phone>' to the 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
钩子将在场景概述
的每次循环迭代结束时执行。
测试执行
在 squishide
中,用户可以执行 Feature
中的所有 Scenarios
,或者仅执行选中的一项 Scenario
。要执行所有 Scenarios
,需要在“测试套件视图”中点击 播放 按钮,相应的测试用例将执行。
要执行单个 Scenario
,需要打开 Feature
文件,在指定的 Scenario
上右键单击并选择 运行 Scenario。另一种方法是点击 播放 按钮旁边的按钮,该按钮位于“测试用例资源”中的 Scenarios 选项卡相应的 Scenario
旁边。
执行 Scenario
之后,根据执行结果会为 Feature
文件着色。更详细的信息(如日志)可以在测试结果视图中找到。
测试调试
Squish 提供了在任何位置暂停测试用例执行的可能性,以便检查脚本变量,监视应用程序对象或在内 Squish 脚本控制台 中运行自定义代码。为此,需要在前台代码中设置一个断点,无论是 Feature
文件的任何包含步骤的行还是任何执行代码的行(即步骤定义代码的中间部分)。
达到断点后,可以检查所有应用程序对象及其属性。如果断点位于步骤定义或在钩子处被到达,则还可以添加验证点或记录代码片段。
重用步骤定义
通过在另一个目录中重用步骤定义,可以增加 BDD 测试的可维护性。关于更多信息,请参阅 collectStepDefinitions()。
教程:现有测试迁移到 BDD
本章针对拥有现有 Squish 测试并希望引入行为驱动测试的用户。第一部分描述了如何保留现有测试,并使用 BDD 方法添加新测试。第二部分描述了如何将现有测试转换为 BDD。
扩展现有测试到 BDD
第一个选项是保持任何现有的基于 Squish 脚本的测试,并通过添加新的 BDD 测试来扩展它们。可以是同时包含基于脚本的测试用例和BDD 测试的 Test Suite
。简单地打开一个包含测试用例的现有 Test Suite
并从下拉列表中选择 新 BDD 测试用例。
假设您现有的测试用例使用库并调用共享函数与 AUT 进行交互,这些函数也可以从 BDD 测试用例中使用。以下示例中,一个函数被用于多个基于脚本的测试用例。
def createNewAddressBook():
mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem))
function createNewAddressBook(){
mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem));
}
sub createNewAddressBook{
mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem));
}
def createNewAddressBook
mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem))
end
新的 BDD 测试用例可以轻松使用相同的函数
@When("I create a new address book")
def step(context):
createNewAddressBook()
When("I create a new address book", function(context){ createNewAddressBook() });
When("I create a new address book", sub { createNewAddressBook(); });
When("I create a new address book") do |context| createNewAddressBook end
将现有测试转换为 BDD
第二个选项是将包含脚本测试用例的现有 Test Suite
转换为行为驱动测试。由于 Test Suite
可以包含两者,因此迁移可以逐步进行。一个包含混合测试用例类型的 Test Suite
可以执行而不需要任何额外的工作,并分析结果。
第一步是审查现有 测试套件
中所有的测试用例,并按它们测试的 功能
进行分组。每个基于脚本的测试用例将转换为 场景
,这是 功能
的一部分。例如,假设我们有 5 个基于脚本的测试用例。经过审查后,我们发现它们检查了两个 功能
。因此,当迁移完成时,我们的测试套件将包含两个 BDD 功能文件,每个文件都包含一个 功能
。每个 功能
将包含多个 场景
。在我们的例子中,第一个 功能
包含三个 场景
,第二个 功能
包含两个 场景
。
一开始,在 squishide
中打开包含要迁移到 BDD 的脚本测试的 测试套件
。然后,通过上下文菜单选择 新建 BDD 测试用例 来创建一个新的测试用例。每个 BDD 测试对应一个包含一个 功能
的 test.feature
文件。接下来,打开 test.feature
文件,使用 Gherkin 语言描述 功能
。按照模板中的语法,编辑 功能
名称,并可选提供简短描述。接下来,分析测试用例中哪些操作和验证需要迁移。以下是一个地址簿应用的示例测试用例。
def main(): startApplication("SquishAddressBook") mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem)) test.compare(waitForObjectExists(names.address_Book_Untitled_NSTableView).numberOfRows(), 0)
function main() { startApplication("SquishAddressBook"); mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem)); test.compare((waitForObjectExists(names.addressBookUntitledNSTableView)).numberOfRows(), 0); }
sub main { startApplication("SquishAddressBook"); mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem)); test.compare(waitForObjectExists($Names::address_book_untitled_nstableview).numberOfRows(), 0) }
def main startApplication("SquishAddressBook") mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem)) Test.compare(waitForObjectExists(Names::Address_Book_Untitled_NSTableView).numberOfRows(), 0)
在分析上述基于脚本的测试之后,我们可以创建以下 场景
并将其添加到 test.feature
Scenario: Initial state of the created address book
Given the address book application is running
When I create a new address book
Then the address book should be empty
接下来,右键单击 场景
,并从上下文菜单中选择 创建缺失步骤实现 选项。这将为 步骤
定义创建骨架。
@Given("the address book application is running") def step(context): test.warning("TODO implement the address book application is running") @When("I create a new address book") def step(context): test.warning("TODO implement I create a new address book") @Then("the address book should be empty") def step(context): test.warning("TODO implement the address book should be empty")
Given("the address book application is running", function(context) { test.warning("TODO implement the address book application is running"); }); When("I create a new address book", function(context) { test.warning("TODO implement I create a new address book"); }); Then("the address book should be empty", function(context) { test.warning("TODO implement the address book should be empty"); });
Given("the address book application is running", sub { my $context = shift; test::warning("TODO implement the address book application is running"); }); When("I create a new address book", sub { my $context = shift; test::warning("TODO implement I create a new address book"); }); Then("the address book should be empty", sub { my $context = shift; test::warning("TODO implement the address book should be empty"); });
Given("the address book application is running") do |context| Test.warning "TODO implement the address book application is running" end When("I create a new address book") do |context| Test.warning "TODO implement I create a new address book" end Then("the address book should be empty") do |context| Test.warning "TODO implement the address book should be empty" end
现在我们将脚本文本测试用例中的代码片段放入相应的步骤定义中,并删除包含 test.warning
的行。如果您的脚本测试使用共享脚本,您也可以在步骤定义中调用这些函数。例如,最终结果可能如下所示。
@Given("the address book application is running") def step(context): startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/mac/addressbook/SquishAddressBook"') test.compare(waitForObjectExists(names.o_NSWindow).isVisible, 0) @When("I create a new address book") def step(context): mouseClick(waitForObject(names.address_Book_Untitled_New_NSToolbarItem)) @Then("the address book should be empty") def step(context): #test.compare(waitForObjectExists(names.address_Book_Untitled_NSTableView).alignment, 0) test.compare(waitForObjectExists(names.address_Book_Untitled_NSTableView).numberOfRows(), 0)
Given("addressbook application is running", function(context) { startApplication('"' + OS.getenv("SQUISH_PREFIX") + '/examples/mac/addressbook/SquishAddressBook"'); test.compare(waitForObjectExists(names.addressBookUntitledNSWindow).isVisible, 1); }); When("I create a new addressbook", function(context) { mouseClick(waitForObject(names.addressBookUntitledNewNSToolbarItem)); }); Then("addressbook should have zero entries", function(context) { //test.compare((waitForObject(names.addressBookUntitledNSTableView)).alignment, 0); test.compare((waitForObject(names.addressBookUntitledNSTableView)).numberOfRows(), 0); });
Given("the address book application is running", sub { my $context = shift; startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/mac/addressbook/SquishAddressBook\""); test::compare(waitForObjectExists($Names::address_book_untitled_nswindow)->isVisible, 1); }); When("I create a new address book", sub { my $context = shift; mouseClick(waitForObject($Names::address_book_untitled_new_nstoolbaritem)); }); Then("the address book should be empty", sub { my $context = shift; #test::compare(waitForObjectExists($Names::address_book_untitled_nstableview)->alignment, 1); test::compare(waitForObjectExists($Names::address_book_untitled_nstableview)->numberOfRows, 0); });
Given("the address book application is running") do |context| startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/mac/addressbook/SquishAddressBook\"") Test.compare(waitForObjectExists(Names::O_NSWindow).isVisible, 0) end When("I create a new address book") do |context| mouseClick(waitForObject(Names::Address_Book_Untitled_New_NSToolbarItem)) end Then("the address book should be empty") do |context| #Test.compare(waitForObjectExists(Names::Address_Book_Untitled_NSTableView).alignment, 0) Test.compare(waitForObjectExists(Names::Address_Book_Untitled_NSTableView).numberOfRows(), 0) end
注意,在将此脚本测试迁移到 BDD 时移除了 test.log("Create a new address book")
。当执行 我创建一个新的地址簿
步骤时,步骤名称将被记录到测试结果中,因此 test.log
调用将是多余的。
在测试用例执行结束时,Squish 终止 AUT。Squish 还确保在每个 场景
结束时终止 AUT。这是通过自动生成的 OnScenarioEnd
钩子完成的。
@OnScenarioEnd def hook(context): for ctx in applicationContextList(): ctx.detach()
OnScenarioEnd(function(context) { applicationContextList().forEach(function(ctx) { ctx.detach(); }); });
# Detach (i.e., potentially terminate) all AUTs at the end of a scenario OnScenarioEnd(sub { foreach (applicationContextList()) { $_->detach(); } });
OnScenarioEnd do |context| applicationContextList().each { |ctx| ctx.detach() } end
上述示例是为了本教程而简化的。为了充分利用 Squish 中的行为驱动测试,请熟悉 行为驱动测试 部分,该部分可在 API 参考手册 中找到。
©2024 Qt 公司有限公司。此处包含的文档贡献是各自所有者的版权。
此处提供的文档根据自由软件基金会出版的 GNU 自由文档许可证版本 1.3 的条款许可。
Qt 和相关徽标是芬兰和/或其他国家/地区的 Qt 公司的商标。所有其他商标都是其各自所有者的财产。