Java (AWT/Swing) Squish教程

学习如何测试基于Java AWT/Swing的应用程序。

有关如何测试其他类型的Java应用程序或创建行为驱动开发测试的更多信息,请参阅

教程:开始测试Java AWT/Swing应用程序

Squish附带IDE和命令行工具。使用squishide是最简单、最好的入门方式,但一旦您建立了大量测试,您将希望自动化它们。例如,进行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或外壳脚本运行的命令行工具是值得的。

注意:如果您需要视频指导,可以在Qt Academy上的45分钟Squish基本用法在线课程上找到更多关于Squish的信息。

我们将要测试的应用程序是一个非常简单的地址簿应用程序。用户可以通过对话框添加新地址,就地编辑地址,并删除地址。他们还可以打开和保存地址簿数据文件。尽管该应用程序非常简单,但它具有您可能会在自己的测试中使用的所有标准功能,包括菜单、表格和一个带有行编辑和按钮的弹出对话框。一旦您知道如何测试这些用户界面元素,您将能够将相同的原理应用到测试您应用程序中的元素,这些元素在本教程中未被使用,例如树视图和数字和日期选择器。有关如何测试列表、表格和树以及最常见的组件(包括选择器)的更多示例,请参阅如何测试应用程序

截图显示了用户添加新的姓名和地址时应用程序的操作。

"The Java AWT/Swing \c {addressbook} example"

该应用程序(即AUT——应用程序被测试)可以在Squish示例的SQUISHDIR/examples/java/addressbook/中找到。在那里,您将在AddressBookSwing.jar中找到AUT。如果您自己编译,可以使用AddressBook.class作为AUT,因为它也包含main()函数。您还可以使用批处理文件或外壳脚本。一些可执行启动器甚至可以作为AUT。以下部分中我们将讨论的测试位于子目录中,例如使用Python编写的测试版本位于SQUISHDIR/examples/java/addressbook/suite_py,而用其他语言编写的测试位于同名子目录。

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

原则上,测试Java AWT/Swing和Java SWT应用程序的工作方式相同,因此本教程中描述的所有实践都可以应用于这两种方法。唯一的显著差异是这两个工具包使用它们自己独特的一组小部件,具有不同的API(应用程序编程接口),因此当然,在我们的测试中必须访问工具包特定的小部件,并在我们想要与它们交互时使用工具包特定的API——例如,检查特定小部件的属性是否具有特定值。

使用示例

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

Squish概念

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

要进行测试,您需要

  1. 一个要测试的应用程序,称为正在测试的应用程序(AUT)。
  2. 一个对AUT进行操作的测试脚本。

Squish的方法的一个基本方面是,AUT和对其进行的测试脚本总是在两个单独的进程中执行。这确保即使AUT崩溃,也不会导致Squish崩溃。在这种情况下,测试脚本将优雅地失败并记录错误信息。除了将Squish和测试脚本与AUT崩溃隔离开外,在单独的进程中运行AUT和测试脚本还具有其他好处。例如,它使得将测试脚本存储在中央位置以及在不同机器和平台上执行远程测试变得更容易。进行远程测试的能力对于在多个平台和嵌入式设备上运行的AUT的测试特别有用。

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

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

以下图说明了单个Squish工具如何协同工作。

"Squish tools"

从测试工程师的角度来看,这种分离是不明显的,因为所有的通信都在幕后自动处理。

可以使用squishide编写和执行测试,在这种情况下,squishserver将自动启动和停止,并且在squishide's Test Results view中显示测试结果。以下图说明了当使用squishide时幕后发生的事情。

"Squish IDE"

Squish工具也可以在不需要squishide的情况下从命令行使用。如果您喜欢使用自己的工具,例如您最喜欢的编辑器,或者希望执行自动批量测试,这将很有用。例如,在夜间运行回归测试时。在这种情况下,必须手动启动squishserver,并在所有测试完成后停止,或者为每个测试单独启动和停止。

注意: Squish 文档在指代 GUI 对象,如按钮、菜单、菜单项、标签和表格控件时,通常使用术语 小部件。Windows 用户可能更熟悉“控件”和“容器”这些术语,但在这里我们使用“小部件”这个词来表示两者。同样,macOS 用户可能习惯于使用“视图”这一术语。

创建可测试的应用程序

在大多数情况下,不需要做任何特殊操作来使应用程序可测试,因为工具包的 API(例如,AWT,SWT)提供了足够的功能来实现和记录测试脚本。当 squishide 启动 AUT 时,也会自动建立到 squishserver 的连接。

创建测试套件

测试套件是一组一个或多个测试用例(测试)。使用测试套件很方便,因为它可以轻松地在相关测试组之间共享脚本和测试数据。

在此处,以及整个教程中,我们首先将描述如何使用 squishide 来完成某些操作,然后是命令行用户的说明。

使用 squishide 创建测试套件

通过单击或双击 squishide 图标、从任务栏菜单启动 squishide 或在命令行上执行 squishide 的方式启动 squishide,具体取决于你的选择和适合你所使用平台的操作。启动 Squish 后,你可能看到一个 欢迎页面。点击右上角的 工作台 按钮以关闭它。然后,squishide 将看起来类似于下面的截图,但可能根据你使用的窗口系统、颜色、字体和主题而略有不同。

"The Squish IDE with no Test Suites"

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

"Name & Directory page"

为您的测试套件输入一个名称,并选择要存储测试套件的文件夹。在截图中,我们将测试套件命名为 suite_py 并将其放入 addressbook 文件夹中。(对于你自己的测试,你可能使用更有意义的名称,比如“suite_addressbook”;我们选择了“suite_py”,因为在教程中我们将创建几个套件,每个套件针对 Squish 支持的每种脚本语言。)当然,你可以选择你喜欢的任何名称和文件夹。一旦完成详细信息,点击 下一步 进入工具包(或脚本语言)页面。

"Toolkit page"

如果你得到这个向导页面,点击你的 AUT 所使用的工具包。对于这个例子,我们必须点击 Java,因为我们正在测试一个 Java 应用程序——无论 AUT 是 AWT/Swing、JavaFX 还是 SWT,Java 选项都涵盖了它们。然后点击 下一步 进入脚本语言页面。

"Scripting Language page"

选择你想要的任何脚本语言 —— 唯一的约束是每个测试套件只能使用一种脚本语言。(所以如果你想使用多种脚本语言,只需创建多个测试套件,每个套件针对你想要使用的每种脚本语言。)Squish 为所有语言提供相同的功能。选择脚本语言后,点击一次 下一步 以获得向导的最后一页。

"AUT page"

如果您正在为一个已知AUT创建新的测试套件,只需单击下拉菜单,就可以弹出AUT列表并选择所需的AUT。如果下拉菜单为空或您的AUT未列出,请单击下拉菜单右侧的浏览按钮—这将从文件打开对话框中弹出,您可以从中选择AUT。对于Java程序,AUT通常是包含所有适合通过双击启动的应用程序类的.jar文件—in this case the AddressBook.jar。选择AUT后,单击完成,Squish将创建一个与测试套件同名的子文件夹,并在其中创建一个名为suite.conf的文件,该文件包含测试套件的配置详细信息。Squish还将AUT注册到squishserver。然后向导将关闭,squishide将类似于下面的截图。

"The Squish IDE with the suite_py test suite"

我们现在可以开始创建测试了。继续阅读以了解如何创建不使用squishide的测试套件,或者跳到录制测试和验证点

从命令行创建测试套件

要从命令行创建新的测试套件

  1. 创建一个新目录来保存测试套件—该目录的名称应从suite开始。在这个示例中,我们为Python测试创建了SQUISHDIR/examples/java/addressbook/suite_py目录。(我们还有其他语言的类似子目录,但这只是为了示例,因为通常我们只使用一种语言进行所有测试。)
  2. 在套件子目录中创建一个名为suite.conf的纯文本文件(ASCII或UTF-8编码)。这是测试套件的配置文件,并且至少必须标识AUT、用于测试的脚本语言以及AUT使用的包装器(即GUI工具包或库)。文件格式是key = value,每行一个键值对。例如
    AUT            = AddressBookSwing.jar
    LANGUAGE       = Python
    WRAPPERS       = Java
    OBJECTMAPSTYLE = script

    Java程序中的AUT是包含AUT类并可以通过双击运行的.jar文件。语言可以设置为您喜欢的任何一种—目前Squish能够支持JavaScript、Python、Perl、Ruby和Tcl。对于Java AWT/Swing和Java SWT程序,将 wrappers 设置为Java就足够了。

  3. 将AUT注册到squishserver。

    注意:每个AUT都必须在squishserver上注册,以便测试脚本不需要包含AUT的路径,从而使测试不依赖于平台。注册的另一个好处是可以在不使用squishide的情况下测试AUT—例如,在进行回归测试时。

    这可以通过在命令行上使用带--config选项和addAUT命令的squishserver来实现。例如,假设我们位于Linux上的squish目录中

    squishserver --config addAUT AddressBookSwing.jar \
    SQUISHDIR/examples/java/addressbook

    Windows用户可以使用\或/作为目录分隔符。

    我们必须给定addAUT命令的AUT的可执行文件名以及—分别地—AUT的路径。在这种情况下,路径是作为测试套件配置文件中添加的AUT的.jar文件的路径。有关应用程序路径的更多信息,请参阅AUT和设置

录制测试和验证点

使用为测试套件指定的脚本语言记录 Squish 测试。一旦记录了一个测试,我们就可以运行该测试,Squish 将忠实地重复我们在记录测试时执行的所有操作,但不需要人类容易出现的暂停。编辑记录的测试或者将记录测试的部分拷贝到手动创建的测试中也是很常见且可行的,我们将在后面的教程中看到这些。

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

  • 选择 文件 > 新建测试用例,打开新 Squish 测试用例向导,输入测试用例名称,然后选择 完成
  • 点击 新建脚本测试用例 ( ) 工具栏按钮,这个按钮位于 测试套件 视图中 测试用例 标签的右侧。这会创建一个新的测试用例,它有一个默认的名字,你可以很容易地更改它。

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

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

"The tst_general test case"

如果你得到了一个 .feature 样本文件而不是 "Hello World" 脚本,点击 运行测试套件 ( ) 左侧的箭头,然后选择 新建脚本测试用例 ( )。

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

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

最初,脚本中的 main()Hello World 记录到测试结果中。为了手动创建一个测试,就像我们将在后面的教程中所做的那样,我们必须创建一个 main 函数,并且应该在上部导入相同的导入。对于 Squish,main 的名字是特殊的。测试可以包含你喜欢的任意数量的函数和其他代码,只要它们支持脚本语言。但是,当测试执行(即运行)时,Squish 总是执行 main 函数。你可以像在如何创建和使用共享数据与共享脚本中描述的那样,在测试脚本之间共享常用的代码。

另外还有两个在 Squish 中特殊的函数名:cleanupinit。更多信息,请参阅测试者创建的特殊函数

一旦创建了新的测试用例,我们就可以手动编写测试代码或录制测试。点击测试用例中的录制)按钮将用新的录制取代测试代码。或者,您可以录制片段并将其插入现有测试用例中,具体操作请参考如何编辑和调试测试脚本

从命令行创建测试

要从命令行创建新的测试用例

  1. 在测试套件目录内创建一个新子目录。例如,在SQUISHDIR/examples/java/addressbook/suite_py目录内创建tst_general目录。
  2. 在测试用例的目录下创建一个名为test.py的文件(如果您使用JavaScript脚本语言,则创建test.js,其他语言同理)。

记录我们的第一个测试

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

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

我们现在准备记录我们的第一个测试。点击测试套件视图的测试用例列表中显示的tst_general测试用例右侧的录制)。这将使Squish运行AUT,以便我们可以与之交互。一旦AUT开始运行,执行以下操作——不要担心耗时,因为Squish不会记录空闲时间

  1. 点击文件 > 打开,然后在文件对话框中,将MyAddresses.adr输入到行编辑框中,然后点击打开按钮。
  2. 点击第二行,然后在添加对话框的第一行编辑框中键入"Jane"。现在点击(或使用Tab键移动到)第二行编辑,并键入"Doe"。继续类似操作,设置电子邮件地址为"[email protected]"和电话号码为"555 123 4567"。不要担心按键错误——只需正常使用退格键删除并修正它们。最后,点击确定按钮。现在应该有一个新的第二个地址,包含你输入的详细信息。
  3. 点击第四行的姓氏列的第二列,删除其文字并替换为"Doe"。然后按Enter键确认你的编辑。
  4. 现在点击第一行,然后点击编辑 > 删除,然后在消息框中点击按钮。第一行应该消失了,所以你的"Jane Doe"条目现在应该是第一个。
  5. 点击Squish控制栏中的验证工具栏按钮并选择属性

    "Squish Control Bar"

    这将弹出squishide。在应用程序对象视图中,使用对象选择器)或展开AddressBook对象,然后是JRootPane对象,然后是JLayeredPane对象,然后是JPanel对象,然后是JScrollPane对象,然后是JViewport对象,最后是JTable对象。现在展开row_0对象。点击column_0对象以使其属性在属性视图中显示,然后检查text属性复选框。

    在这个步骤,请再次确认类型设置为脚本化属性,以防之前设置过其他选项。

    接下来,点击column_1对象并检查其文本属性。

    最后,在验证点创建器中点击保存并插入验证按钮,将插入第一个要插入到记录测试脚本中的行的前名和姓氏验证。一旦验证点被插入,squishide的窗口将再次隐藏,控制栏窗口和AUT将恢复显示。

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

"Two verification points about to be inserted"

退出AUT后,记录的测试将如屏幕截图显示在squishide中。(注意,记录的准确代码将根据您的交互方式而有所不同。例如,您可以通过点击或使用快捷键调用菜单选项——无论是哪种方式,都不重要,但是因为它们是不同的,Squish将会以不同的方式记录它们。)

"The Squish IDE showing recorded scriptified verifications"

如果记录的测试没有出现,请点击(或根据您的平台和设置双击)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自动使用

当您连接到多个设备和/或模拟器时,您需要使用--device some-device来指定目标

从IDE运行测试

squishide 中运行测试用例,当将鼠标悬停或选择测试用例时,单击 运行测试 ( )。

要依次运行两个或多个测试用例,或仅运行所选测试用例,请单击 运行测试套件 ( )。

从命令行运行测试

在运行测试时,squishserver必须始终处于运行状态,或者需要为squishrunner提供--local选项。欲了解更多信息,请参阅squishserver

要从命令行回放已记录的测试,我们执行squishrunner程序,并指定包含记录脚本的测试套件以及我们想要回放的测试用例。例如,假设我们位于包含测试套件目录的目录中

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

检查生成的代码

如果您查看截图中的代码(或下面的代码片段),您将看到它包含大量Object waitForObject(objectOrName)调用,作为各种其他调用(如activateItem(objectOrName, itemText)clickButton(objectOrName)mouseClick(objectOrName)type(objectOrName, text))的参数。Object waitForObject(objectOrName)函数等待GUI对象准备好交互(即变为可见和启用),随后跟随一些与对象交互的函数。典型交互包括激活(弹出)菜单、单击菜单选项或按钮,或者输入某些文本。

要查看Squish的脚本命令的完整概述,请参阅如何创建测试脚本如何测试应用程序 - 专用细节API参考工具参考

对象通过Squish生成的名称识别。请参阅如何标识和访问对象以获取完整详情。

生成的代码大约有35行。以下是从记录点击“编辑”菜单的添加选项、将Jane Doe的详细信息输入到添加对话框,并在最后点击“确定”以关闭对话框并更新表格的示例。

注意:尽管截图仅显示Python测试套件正在运行,但对于本教程中引用的代码片段,我们显示了Squish支持的脚本语言代码。在实际应用中,您通常只会使用其中之一,因此请随意查看您感兴趣的语法的代码片段,并跳过其他语言。

    activateItem(waitForObjectItem(names.address_Book_MyAddresses_adr_JMenuBar, "Edit"))
    activateItem(waitForObjectItem(names.edit_JMenu, "Add..."))
    type(waitForObject(names.address_Book_Add_Forename_JTextField), "Jane")
    type(waitForObject(names.address_Book_Add_Surname_JTextField), "Doe")
    type(waitForObject(names.address_Book_Add_Email_JTextField), "[email protected]")
    type(waitForObject(names.address_Book_Add_Phone_JTextField), "123 555 1212")
    clickButton(waitForObject(names.address_Book_Add_OK_JButton))
    activateItem(waitForObjectItem(names.addressBookMyAddressesAdrJMenuBar, "Edit"));
    activateItem(waitForObjectItem(names.editJMenu, "Add..."));
    type(waitForObject(names.addressBookAddForenameJTextField), "Jane");
    type(waitForObject(names.addressBookAddSurnameJTextField), "Doe");
    type(waitForObject(names.addressBookAddEmailJTextField), "[email protected]");
    type(waitForObject(names.addressBookAddPhoneJTextField), "123 555 1212");
    clickButton(waitForObject(names.addressBookAddOKJButton));
    activateItem(waitForObjectItem($Names::address_book_myaddresses_adr_jmenubar, "Edit"));
    activateItem(waitForObjectItem($Names::edit_jmenu, "Add..."));
    type(waitForObject($Names::address_book_add_forename_jtextfield), "Jane");
    type(waitForObject($Names::address_book_add_surname_jtextfield), "Doe");
    type(waitForObject($Names::address_book_add_email_jtextfield), "jane.doe\@nowhere.com");
    type(waitForObject($Names::address_book_add_phone_jtextfield), "123 555 1212");
    clickButton(waitForObject($Names::address_book_add_ok_jbutton));
    activateItem(waitForObjectItem(Names::Address_Book_MyAddresses_adr_JMenuBar, "Edit"))
    activateItem(waitForObjectItem(Names::Edit_JMenu, "Add..."))
    type(waitForObject(Names::Address_Book_Add_Forename_JTextField), "Jane")
    type(waitForObject(Names::Address_Book_Add_Surname_JTextField), "Doe")
    type(waitForObject(Names::Address_Book_Add_Email_JTextField), "[email protected]")
    type(waitForObject(Names::Address_Book_Add_Phone_JTextField), "123 555 1212")
    clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
    invoke activateItem [waitForObjectItem $names::Address_Book_MyAddresses_adr_JMenuBar "Edit"]
    invoke activateItem [waitForObjectItem $names::Edit_JMenu "Add..."]
    invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] "Jane"
    invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] "Doe"
    invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] "[email protected]"
    invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] "123 555 1212"
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]

为了简洁起见,已经编辑了此脚本。最初,测试人员使用键盘在文本字段之间进行制表,但我们删除了这些交互,因为它们对于回放不是必需的。如果测试人员通过单击鼠标切换焦点,并通过按空格键在确定按钮上制表,或者任何其他交互组合,结果将相同,但当然Squish将记录实际执行的操作。

在代码片段中请注意,没有显式的延迟。(可以使用Squish的snooze(seconds)函数强制延迟。)这是因为Object waitForObject(objectOrName)函数会在给定的对象准备好之前延迟——从而使Squish的运行速度与GUI工具包可以处理的速度一样快,但不会更快。

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

当符号名称在光标下方时,编辑器的上下文菜单允许您打开符号名称,显示其对象映射中的条目,或转换为真实名称,这在您的脚本语言中放置了一个内联映射,允许您在脚本本身中手动编辑属性。

现在我们已经看到了如何记录测试并将其回放,也看到了Squish生成的代码,让我们更进一步,确保测试执行中特定点的某些条件成立。

插入额外的验证点

在上一节中,我们看到了在录制测试脚本期间插入验证点有多容易。验证点也可以通过设置断点和使用squishide或将调用Squish的测试函数(如Boolean test.compare(value1, value2)Boolean test.verify(condition))来编辑现有的测试脚本而插入。

Squish支持许多种类的验证点:那些验证对象属性具有特定值——称为“对象属性验证”;那些验证整个表具有我们期望的内容——称为“表格验证”;那些验证两个图像匹配——称为“屏幕截图验证”;以及一个包含来自多个对象的属性和屏幕截图的混合验证类型,称为“视觉验证”。此外,还可以验证搜索图像是否出现在屏幕上的某个位置,或者通过OCR找到某些文本。最常用的是对象属性验证,这部分将是教程中覆盖的内容。有关进一步阅读,见如何创建和使用验证点)。

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

脚本化属性验证点是直接调用布尔测试.compare(value1, value2)函数,包含两个参数—特定对象的特定属性的值和一个期望值。我们可以在记录或手写的脚本中手动添加对布尔测试.compare(value1, value2)函数的调用,或者我们可以让Squish自动插入它们,使用脚本化验证点。在前一个部分,我们展示了如何使用squishide在记录过程中插入验证。在这里,我们将首先展示如何使用squishide将验证插入到现有的测试脚本中,然后我们将展示如何手动插入验证。

在请Squish插入验证点之前,最好先确定我们想要验证哪些内容以及什么时候。我们可以在tst_general测试用例中添加许多潜在的验证,但由于我们在这里的目的只是展示如何操作,所以我们将只进行两个验证—我们将验证“Jane Doe”条目的电子邮件地址和电话号码与输入的一致,并将验证放在我们记录时插入的验证之后。

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

"The tst_general test case with a breakpoint"

如上面的截图所示,我们在第36行设置了断点。这是通过双击或右键单击编辑器旁边的空白区域(行号旁边),选择添加断点上下文菜单项来完成的。我们选择这一行,因为它跟随着删除第一个地址的脚本行,因此在这个位置(在调用文件菜单来关闭应用程序之前),第一个地址应该是“Jane Doe”的地址。截图显示了在记录时使用squishide输入的验证。我们的附加验证将随后进行。但是,如果你的行号有所不同,那是因为你用不同的方式记录了测试,例如,使用键盘快捷键而不是点击菜单项。

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

视角和视图

squishide工作得就像Eclipse IDE一样。如果你不熟悉Eclipse,理解以下关键概念是至关重要的:视图视角。在Eclipse中,以及因此也在squishide中,一个视图基本上是一个子窗口,如一个停靠窗口或在现有窗口中的一个标签。一个视角是一组一起排列的视图。两者都可以通过窗口菜单访问。

squishide附带以下视角:

您可以更改这些视角以显示附加视图或隐藏不需要的视图,或者创建自己的视角,其中包含您想要的精确视图。所以如果您的窗口发生了巨变,这只意味着视角已经更改。使用 窗口 菜单切换回您想要的视角。但是,Squish会自动更改视角以反映当前情况,因此您通常不需要手动更改视角。

插入验证点

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

要插入验证点,我们可以展开 应用程序对象 视图中的条目,直到找到要验证的对象,或者可以使用 对象选择器 ( )。在这个例子中,我们想验证 JTable 的第一行的文本,因此我们展开 AddressBook 项,以及其子项,直到我们找到 JTable,然后在其中找到我们感兴趣的行列。一旦点击列对象,其属性就会如截图所示在 属性视图 中显示。

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

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

在这里,我们可以看到行0列0中项目的主属性值是 "Jane";我们已经在这个过程中插入了一个验证。向下滚动,以便可以看到行0列2的项目:这是电子邮件地址。为了确保每次运行测试时都进行验证,请点击 应用程序对象视图 中的 column_2 项,使其属性显示,然后点击 text 属性并检查它的复选框。点击它后,如截图所示,会出现 验证点创建视图

"Choosing a property value to verify"

目前这个验证点还没有添加到测试脚本中。我们可以通过点击 保存并插入验证 按钮轻松添加它。但在做之前,我们将添加一个需要验证的额外项目。

向下滚动,并点击 应用程序对象视图 中的 column_3 项;然后点击它的 text 属性。现在两个验证都会如截图所示出现在 验证点创建视图 中。

"Choosing several property values to verify"

我们已声明我们希望这些属性具有显示的值,即电子邮件地址为 "[email protected]",电话号码为 "555 123 4567"。我们必须点击 插入 按钮来实际上插入验证点,因此现在请这么操作。

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

完成插入验证并停止或完成测试运行后,我们现在应该禁用断点。只需右键单击断点,然后在上下文菜单中选择禁用断点菜单选项。我们现在可以准备运行不带断点但有验证点的测试。点击运行测试)按钮。这次我们将获得一些额外的测试结果——如图表所示,我们扩大其中之一以显示其详细信息。(我们还将选择Squish在录制过程中插入以执行验证的代码行——请注意,代码的结构与录制期间插入的代码相同。)

"The newly inserted verification points"

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

另一种插入验证点的方法是在代码形式中插入它们。理论上,我们可以在现有脚本中的任何位置添加自己的函数调用,例如Squish的测试函数,如Boolean test.compare(value1, value2)Boolean test.verify(condition)。在实际操作中,最好确保Squish知道我们想要验证的对象,以便在测试运行时找到它们。这涉及到一个非常类似的过程,就像使用squishide插入一样。首先,我们在打算添加验证的地方设置一个断点。然后,运行测试脚本直到它停止。接下来,在应用程序对象视图中导航,直到找到我们想要验证的对象。此时,点击我们感兴趣的对象并单击上下文菜单中的添加到对象映射是一个明智的选择。这将确保Squish可以访问对象。然后,再次右键单击并单击上下文菜单中的复制符号名。这将给我们的对象一个名字,Squish将用它来识别它。现在我们可以在测试脚本中添加我们的验证内容并完成或停止执行。(记住,一旦不再需要,就禁用断点。)

尽管我们可以编写与自动生成代码完全相同的代码风格来编写我们的测试脚本代码,但通常以略有不同的风格做事会更清晰、更简单,我们将在下面解释。

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

"Manually entered verification points"

在编写脚本时,我们使用Squish的test模块的函数在测试脚本执行的某些点比较或验证条件。如截图(以及下面的代码片段)所示,我们首先检索我们感兴趣的对象的引用。使用Object waitForObject(objectOrName)函数是手动编写的测试脚本的行业标准实践。此函数等待对象可用(即,可见和启用),然后返回对该对象的引用。(否则,它会超时并引发可捕获的异常。)我们然后使用此引用访问项目属性——在这个例子中是JTable的行数——并使用Boolean test.verify(condition)函数验证该值是否为预期的值。(顺便提一下,我们从上一行获得了对象的名称,因此我们不需要设置断点并将表格的名称手动添加到对象映射中,以确保Squish会记住这个特定情况,因为Squish已经在测试录制期间添加了它。)

以下是手动输入的第一个验证所编写的代码,适用于 Squish 支持的所有脚本语言。当然,您只需要查看您自己测试将使用的语言代码。

    activateItem(waitForObjectItem(names.address_Book_MyAddresses_adr_JMenuBar, "Edit"))
    activateItem(waitForObjectItem(names.edit_JMenu, "Add..."))
    type(waitForObject(names.address_Book_Add_Forename_JTextField), "Jane")
    type(waitForObject(names.address_Book_Add_Surname_JTextField), "Doe")
    type(waitForObject(names.address_Book_Add_Email_JTextField), "[email protected]")
    type(waitForObject(names.address_Book_Add_Phone_JTextField), "123 555 1212")
    clickButton(waitForObject(names.address_Book_Add_OK_JButton))

    mouseClick(waitForObjectItem(names.address_Book_MyAddresses_adr_JTable, "3/1"), 54, 4, 0, Button.Button1)
    mouseClick(waitForObjectItem(names.address_Book_MyAddresses_adr_JTable, "3/1"), 54, 3, 0, Button.Button1)
    doubleClick(waitForObjectItem(names.address_Book_MyAddresses_adr_JTable, "3/1"), 54, 3, 0, Button.Button1)
    type(waitForObject(names.o_JTextField), "<Backspace>")
    type(waitForObject(names.o_JTextField), "<Backspace>")
    type(waitForObject(names.o_JTextField), "<Backspace>")
    type(waitForObject(names.o_JTextField), "<Backspace>")
    type(waitForObject(names.o_JTextField), "<Backspace>")
    type(waitForObject(names.o_JTextField), "Doe")
    type(waitForObject(names.o_JTextField), "<Return>")

    mouseClick(waitForObjectItem(names.address_Book_MyAddresses_adr_JTable, "0/0"), 135, 8, 0, Button.Button1)
    test.compare(table.getRowCount(), 126)
    mouseClick(waitForObjectItem(names.addressBookMyAddressesAdrJTable, "1/0"), 124, 8, 0, Button.Button1);
    activateItem(waitForObjectItem(names.addressBookMyAddressesAdrJMenuBar, "Edit"));
    activateItem(waitForObjectItem(names.editJMenu, "Add..."));
    type(waitForObject(names.addressBookAddForenameJTextField), "Jane");
    type(waitForObject(names.addressBookAddSurnameJTextField), "Doe");
    type(waitForObject(names.addressBookAddEmailJTextField), "[email protected]");
    type(waitForObject(names.addressBookAddPhoneJTextField), "123 555 1212");
    clickButton(waitForObject(names.addressBookAddOKJButton));
    test.compare(table.getRowCount(), 126);
    mouseClick(waitForObjectItem($Names::address_book_myaddresses_adr_jtable, "2/1"), 33, 7, 0, Button->Button1);
    mouseClick(waitForObjectItem($Names::address_book_myaddresses_adr_jtable, "2/1"), 33, 7, 0, Button->Button1);
    doubleClick(waitForObjectItem($Names::address_book_myaddresses_adr_jtable, "2/1"), 33, 7, 0, Button->Button1);
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Backspace>");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Backspace>");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Backspace>");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Backspace>");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Backspace>");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "Doe");
    type(waitForObject($Names::address_book_myaddresses_adr_jtextfield), "<Return>");
    activateItem(waitForObjectItem($Names::address_book_myaddresses_adr_jmenubar, "Edit"));
    activateItem(waitForObjectItem($Names::edit_jmenu, "Add..."));
    type(waitForObject($Names::address_book_add_forename_jtextfield), "Jane");
    type(waitForObject($Names::address_book_add_surname_jtextfield), "Doe");
    type(waitForObject($Names::address_book_add_email_jtextfield), "jane.doe\@nowhere.com");
    type(waitForObject($Names::address_book_add_phone_jtextfield), "123 555 1212");
    clickButton(waitForObject($Names::address_book_add_ok_jbutton));
    test::compare($table->getRowCount(), 126);
    activateItem(waitForObjectItem(Names::Address_Book_MyAddresses_adr_JMenuBar, "Edit"))
    activateItem(waitForObjectItem(Names::Edit_JMenu, "Add..."))
    type(waitForObject(Names::Address_Book_Add_Forename_JTextField), "Jane")
    type(waitForObject(Names::Address_Book_Add_Surname_JTextField), "Doe")
    type(waitForObject(Names::Address_Book_Add_Email_JTextField), "[email protected]")
    type(waitForObject(Names::Address_Book_Add_Phone_JTextField), "123 555 1212")
    clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
    Test.compare(table.rowcount, 126)
    invoke mouseClick [waitForObjectItem $names::Address_Book_MyAddresses_adr_JTable "1/0"] 55 10 0 [enum Button Button1]
    invoke activateItem [waitForObjectItem $names::Address_Book_MyAddresses_adr_JMenuBar "Edit"]
    invoke activateItem [waitForObjectItem $names::Edit_JMenu "Add..."]
    invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] "Jane"
    invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] "Doe"
    invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] "[email protected]"
    invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] "123 555 1212"
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]
    test compare [invoke $table getRowCount] 126

编码模式非常简单:我们获取所关注对象的引用,然后使用 Squish 的一种验证函数验证其属性。当然,如果我们愿意,也可以调用对象上的方法与之交互。

有关手动编写代码的更多示例,请参阅手动创建测试如何创建测试脚本,以及如何测试应用程序 - 特殊要求

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

测试结果

每次测试运行完成后,底部 squishide 中的“测试结果”视图将显示测试结果,包括验证点的结果。

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

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

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

手动创建测试

在了解了如何记录测试和如何通过插入验证点来修改之后,我们现在准备了解如何手动创建测试。最简单的方法是修改和重构记录测试,虽然从头开始创建手动测试也是完全可能的。

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

我们可以通过点击 对象映射 工具栏按钮(在测试套件窗口中),或从脚本编辑器上下文菜单中选择在脚本中右键单击对象名称时的 打开符号名称(参见对象映射视图)。Squish 与每个应用对象交互的列表都列在这里,要么是顶级对象,要么是子对象(视图是一个树视图)。我们可以通过右键单击我们感兴趣的对象并点击上下文菜单的 复制对象名称(以获取符号名称变量)或 复制真实名称(以获取存储在变量中的实际键值对)来检索 Squish 在录制的脚本中使用的符号名称。当我们要修改现有的测试脚本或我们要从头创建测试脚本时,这非常有用,正如我们在教程后面将要看到的那样。

"Squish's Object Map"

修改和重构录制的测试

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

首先我们必须创建一个新的测试用例。点击 文件 > 新测试用例,并将其测试用例的名称设置为 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/java/addressbook/AddressBookSwing.jar"')

    activateItem(waitForObjectItem(names.address_Book_JMenuBar, "File"))
    activateItem(waitForObjectItem(names.file_JMenu, "Open..."))
import * as names from 'names.js';

function main() {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook/AddressBookSwing.jar"');
    activateItem(waitForObjectItem(names.addressBookJMenuBar, "File"));
    activateItem(waitForObjectItem(names.fileJMenu, "Open..."));
require 'names.pl';

sub main
{
        startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook/AddressBookSwing.jar\"");
    activateItem(waitForObjectItem($Names::address_book_jmenubar, "File"));
    activateItem(waitForObjectItem($Names::file_jmenu, "Open..."));
require 'squish'
require 'names'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/addressbook/AddressBookSwing.jar\"")
    activateItem(waitForObjectItem(Names::Address_Book_JMenuBar, "File"))
    activateItem(waitForObjectItem(Names::File_JMenu, "Open..."))
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/addressbook/AddressBookSwing.jar\""
    invoke activateItem [waitForObjectItem $names::Address_Book_JMenuBar "File"]
    invoke activateItem [waitForObjectItem $names::File_JMenu "Open..."]

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

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

如果您查看记录的测试用例(tst_general)或在对象映射中,您会看到Squish有时会为相同的事物使用不同的名称。例如,菜单栏以两种不同的方式被识别,最初是 ":Address Book_JMenuBar",后来是 ":Address Book - MyAddresses.adr_JMenuBar"。原因是因为Squish需要在特定上下文中唯一地识别每个对象,它使用手头可用的任何信息。因此,在识别菜单栏(以及许多其他对象)的情况下,Squish使用窗口标题文本来获取某些上下文。(例如,根据应用程序是否加载了文件以及应用程序的状态,应用程序的文件或编辑菜单可能有不同的选项。)

自然,当我们编写测试脚本时,我们不想知道或关心应该使用哪个特定的名称变体,Squish通过提供替代的命名方案来支持这一需求,正如我们很快就会看到的。

如果在测试执行期间AUT似乎冻结了,请等待Squish对AUT进行超时(大约20秒),并显示 Object Not Found 对话框,指出如下此类错误

"Object Not Found dialog"

这通常意味着Squish没有在对象映射中找到给定名称的对象或属性值。从这里,我们可以 选择一个新的对象调试抛出错误,或者在选择了新对象后,重试

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

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

我们现在几乎准备好编写自己的测试脚本了。最简单的方法是从录制一个虚拟测试开始。所以点击文件 > 新建测试用例,并将测试用例的名称设置为tst_dummy。然后点击虚拟测试用例的记录(《img alt="" src="images/record.png">))。一旦AUT开始运行,点击文件 > 新建,然后点击(空的)表格,接着点击编辑 > 添加并添加一个条目,然后按回车或点击确定。最后,点击文件 > 退出以完成,并选择不保存更改。然后仅为了确认一切正常,再次运行此测试。这样做的唯一目的是确保Squish将必要的名称添加到对象映射中,因为这种方法可能比使用每个感兴趣对象的Spy快。

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

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/addressbook/AddressBookSwing.jar"')

    invokeMenuItem("File", "New...")
    jtable = waitForObject({"type": "javax.swing.JTable"})
    test.verify(jtable.rowcount == 0)
    data = [("Andy", "Beach", "[email protected]", "555 123 6786"),
            ("Candy", "Deane", "[email protected]", "555 234 8765"),
            ("Ed", "Fernleaf", "[email protected]", "555 876 4654")]
    for fields in data:
        addNameAndAddress(fields)
    test.compare(jtable.rowcount, len(data))
    closeWithoutSaving()
import * as names from 'names.js';

function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook/AddressBookSwing.jar"');
    invokeMenuItem("File", "New...");
    var jtable = waitForObject({"type": "javax.swing.JTable"});
    test.verify(jtable.getRowCount() == 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(jtable.getRowCount(), data.length);
    closeWithoutSaving();
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook/AddressBookSwing.jar\"");
    invokeMenuItem("File", "New...");
    my $jtable = waitForObject({"type" => "javax.swing.JTable"});
    test::verify($jtable->getRowCount() == 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::compare($jtable->getRowCount(), scalar(@data));
    closeWithoutSaving();
}
require 'squish'
require 'names'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/addressbook/AddressBookSwing.jar\"")
    invokeMenuItem("File", "New...")
    jtable = waitForObject({:type => "javax.swing.JTable"})
    Test.verify(jtable.rowcount == 0)
    data = [["Andy", "Beach", "[email protected]", "555 123 6786"],
            ["Candy", "Deane", "[email protected]", "555 234 8765"],
            ["Ed", "Fernleaf", "[email protected]", "555 876 4654"]]
    data.each do |oneNameAndAddress|
        addNameAndAddress(oneNameAndAddress)
    end
    Test.compare(jtable.rowcount, data.length)
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/addressbook/AddressBookSwing.jar\""
    invokeMenuItem "File" "New..."
    set jtable [waitForObject $names::Address_Book_Unnamed_JTable]
    test verify [expr {[property get $jtable rowcount] == 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 $jtable rowcount] [llength $data]
    closeWithoutSaving
}

我们首先通过调用ApplicationContext startApplication(autName)函数启动应用程序。我们传递的字符串是Squish注册的名称(通常是包含应用程序代码的.jar文件或包含main方法的类的.class文件)。然后我们获得对JTable的引用。我们想要的对象名称尚未在对象映射中,从tst_general获得的JTable的现有对象名称太具体,在测试的这个阶段将不起作用。因此,我们可以采取一个现有的符号名称,该名称与同一类型相关,从编辑器中右键单击它并转换为真实名称,移除对函数不相关或过于具体的功能。

Object waitForObject(objectOrName)函数等待一个对象准备好(可见并启用),并返回对它的引用——或者超时并引发可捕获的异常。

invokeMenuItem函数是为这个测试特别创建的。它接受菜单名称和菜单选项名称,并调用菜单选项。使用invokeMenuItem函数执行文件 > 新建后,我们验证表的行数为0。Boolean test.verify(condition)函数在我们要简单地验证条件为真而不是比较两个不同的值时很有用。(对于Tcl,我们通常使用Boolean test.compare(value1, value2)函数而不是Boolean test.verify(condition)函数,因为这在使用Tcl时稍微简单一些。)

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

现在,我们将审查这三个支持函数中的每一个,以便了解tst_adding测试用例中的所有代码,从invokeMenuItem函数开始。

def invokeMenuItem(menu, item):
    mouseClick(waitForObjectItem({"type": "javax.swing.JMenuBar"}, menu))
    mouseClick(waitForObjectItem({"caption": menu, "type": "javax.swing.JMenu"}, item))
function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"type": "javax.swing.JMenuBar"}, menu));
    activateItem(waitForObjectItem({"caption": menu, "type": "javax.swing.JMenu"}, item));
}
sub invokeMenuItem
{
    my($menu, $item) = @_;
    activateItem(waitForObjectItem({"type" => "javax.swing.JMenuBar"}, $menu));
    activateItem(waitForObjectItem({"caption" => $menu, "type" => "javax.swing.JMenu"}, $item));
}
def invokeMenuItem(menu, item)
    activateItem(waitForObjectItem({:type => "javax.swing.JMenuBar"}, menu))
    activateItem(waitForObjectItem({:caption => menu, :type => "javax.swing.JMenu"}, item))
end
proc invokeMenuItem {menu item} {
    invoke activateItem [waitForObjectItem [::Squish::ObjectName type javax.swing.JMenuBar visible true] $menu]
    invoke activateItem [waitForObjectItem [::Squish::ObjectName type javax.swing.JMenu visible true caption $menu] $item]
}

如我们 earlier 所述,Squish用于菜单和菜单项(以及其他对象)的对象名称可能因上下文而异,并且通常是从窗口标题派生的名称。对于将当前文件名放入标题的应用程序——如地址簿示例——名称将包含文件名,我们必须考虑这一点。

在地址簿示例中,主窗口的标题是“地址簿”(启动时),或者在“文件”>“新建”(但在“文件”>“保存”或“文件”>“另存为”之前),标题为“地址簿 - 未命名”,也可以是“地址簿 - 文件名”,其中“文件名”可以随意更改。我们的代码通过使用真实的多属性名称来处理所有这些情况。

真实名称由脚语言中的键值映射表示。每个真实名称都必须指定类型属性和其他至少一个属性。在这里,我们使用类型来唯一标识MenuBar,并使用类型和标题属性来唯一标识Menu

一旦我们确定了要与之交互的对象,我们就使用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_Forename_JTextField), fields[0])
    type(waitForObject(names.address_Book_Add_Surname_JTextField), fields[1])
    type(waitForObject(names.address_Book_Add_Email_JTextField), fields[2])
    type(waitForObject(names.address_Book_Add_Phone_JTextField), fields[3])
    clickButton(waitForObject(names.address_Book_Add_OK_JButton))
function addNameAndAddress(fields)
{
    invokeMenuItem("Edit", "Add...");
    type(waitForObject(names.addressBookAddForenameJTextField), fields[0]);
    type(waitForObject(names.addressBookAddSurnameJTextField), fields[1]);
    type(waitForObject(names.addressBookAddEmailJTextField), fields[2]);
    type(waitForObject(names.addressBookAddPhoneJTextField), fields[3]);
    clickButton(waitForObject(names.addressBookAddOKJButton));
}
sub addNameAndAddress
{
    invokeMenuItem("Edit", "Add...");
    type(waitForObject($Names::address_book_add_forename_jtextfield), $_[0]);
    type(waitForObject($Names::address_book_add_surname_jtextfield), $_[1]);
    type(waitForObject($Names::address_book_add_email_jtextfield), $_[2]);
    type(waitForObject($Names::address_book_add_phone_jtextfield), $_[3]);
    clickButton(waitForObject($Names::address_book_add_ok_jbutton));
}
def addNameAndAddress(oneNameAndAddress)
    invokeMenuItem("Edit", "Add...")
    type(waitForObject(Names::Address_Book_Add_Forename_JTextField), oneNameAndAddress[0])
    type(waitForObject(Names::Address_Book_Add_Surname_JTextField), oneNameAndAddress[1])
    type(waitForObject(Names::Address_Book_Add_Email_JTextField), oneNameAndAddress[2])
    type(waitForObject(Names::Address_Book_Add_Phone_JTextField), oneNameAndAddress[3])
    clickButton(waitForObject(Names::Address_Book_Add_OK_JButton))
end
proc addNameAndAddress {fields} {
    invokeMenuItem "Edit" "Add..."

    invoke type [waitForObject $names::Address_Book_Add_Forename_JTextField] [lindex $fields 0]
    invoke type [waitForObject $names::Address_Book_Add_Surname_JTextField] [lindex $fields 1]
    invoke type [waitForObject $names::Address_Book_Add_Email_JTextField] [lindex $fields 2]
    invoke type [waitForObject $names::Address_Book_Add_Phone_JTextField] [lindex $fields 3]
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_JButton]
}

对于每一组名称和地址数据,我们通过调用“编辑”>“添加”菜单选项来弹出添加对话框。然后,对于收到的每个值,我们通过等待相关的JTextField准备就绪,然后使用type(objectOrName, text)函数输入文本来填充相应的字段。最后,我们点击对话框的“确定”按钮。我们通过从记录的tst_general测试中复制并简单参数化文本来获取函数的精髓。同样,我们复制了从tst_general测试用例代码中点击确定按钮的代码。

def closeWithoutSaving():
    invokeMenuItem("File", "Quit")
    clickButton(waitForObject(names.address_Book_No_JButton))
function closeWithoutSaving()
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject(names.addressBookNoJButton));
}
sub closeWithoutSaving
{
    invokeMenuItem("File", "Quit");
    clickButton(waitForObject($Names::address_book_no_jbutton));
}
def closeWithoutSaving
    invokeMenuItem("File", "Quit")
    clickButton(waitForObject(Names::Address_Book_No_JButton))
end
proc closeWithoutSaving {} {
    invokeMenuItem "File" "Quit"
    invoke clickButton [waitForObject $names::Address_Book_No_JButton]
}

在此处,我们使用invokeMenuItem函数执行文件 > 退出,然后点击“保存未保存更改”对话框的“否”按钮。最后一行是从记录的测试中复制来的,但将文件名从“MyAddresses.adr”更改到“Unnamed”,因为在测试过程中我们调用了文件 > 新建,但从未保存过文件。(另一种选择是使用实际的(多属性)名称来识别未保存更改对话框的“否”按钮。)

整个测试的代码不超过30行——如果我们将一些公共函数(如invokeMenuItemcloseWithoutSaving)放在共享脚本中,代码将更少。并且大量代码直接从记录的测试中复制而来,在有些情况下进行了参数化。

这应该足以让人们对编写AUT测试脚本的写作有一个印象。请注意,Squish提供了比这里使用多得多的功能(所有这些都在API参考工具参考中有详细介绍)。Squish还提供了对AUT对象的所有公共API的访问。

然而,测试案例的一个方面并不令人满意。尽管像我们这样将测试数据嵌入在这里对少量数据来说是合理的,但对于大量测试数据来说,却相当受限。此外,我们没有测试任何添加到测试数据中的数据,以查看它们是否正确地出现在JTable中。在下一节中,我们将创建这个测试的新版本,这一次我们将从外部数据源获取数据,并检查JTable中的数据是否正确。

创建数据驱动测试

在前一节中,我们在测试中放入了三个硬编码的名称和地址。但如果我们想测试大量的数据怎么办?一种方法是导入一个数据集到Squish中,并使用数据集作为我们插入测试的值的来源。Squish可以导入.tsv(制表符分隔值格式)、.csv(逗号分隔值格式)、.xls.xlsx(Microsoft Excel电子表格格式)。

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

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

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

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

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

"Squish with some imported test data"

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

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

import names
import os

def main():
    startApplication('"' + os.environ["SQUISH_PREFIX"] + '/examples/java/addressbook/AddressBookSwing.jar"')

    jtable = waitForObject({"type": "javax.swing.JTable"})
    invokeMenuItem("File", "New...")
    test.verify(jtable.getRowCount() == 0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
        forename = testData.field(record, "Forename")
        surname = testData.field(record, "Surname")
        email = testData.field(record, "Email")
        phone = testData.field(record, "Phone")
        addNameAndAddress((forename, surname, email, phone)) # pass as a single tuple
        checkNameAndAddress(jtable, record)
        if row > limit:
            break
    test.compare(jtable.getRowCount(), row + 1)
    closeWithoutSaving()
import * as names from 'names.js';

function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook/AddressBookSwing.jar"');

    invokeMenuItem("File", "New...");
    var jtable = waitForObject({"type": "javax.swing.JTable"});
    test.verify(jtable.rowcount == 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(jtable, record);
        if (row > limit)
            break;
    }
    test.compare(jtable.rowcount, row + 1);
    closeWithoutSaving();
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook/AddressBookSwing.jar\"");
    invokeMenuItem("File", "New...");
    my $jtable = waitForObject({"type" => "javax.swing.JTable"});
    test::verify($jtable->getRowCount() == 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($jtable, $record);
        if ($row > $limit) {
            last;
        }
    }
    test::compare($jtable->getRowCount(), $row + 1);
    closeWithoutSaving();
}
require 'squish'
require 'names'

include Squish

def main
    startApplication("\"#{ENV['SQUISH_PREFIX']}/examples/java/addressbook/AddressBookSwing.jar\"")
    jtable = waitForObject({:type => "javax.swing.JTable"})
    invokeMenuItem("File", "New...")
    Test.verify(jtable.rowcount == 0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    rows = 0
    TestData.dataset("MyAddresses.tsv").each_with_index do
        |record, row|
        forename = TestData.field(record, "Forename")
        surname = TestData.field(record, "Surname")
        email = TestData.field(record, "Email")
        phone = TestData.field(record, "Phone")
        addNameAndAddress([forename, surname, email, phone]) # pass as a single Array
        checkNameAndAddress(jtable, record)
        break if row > limit
        rows += 1
    end
    Test.compare(jtable.rowcount, rows + 1)
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

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

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

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

def checkNameAndAddress(jtable, record):
    tableModel = jtable.getModel()
    for column in range(len(testData.fieldNames(record))):
        value = tableModel.getValueAt(0, column)
        test.compare(value, testData.field(record, column))
function checkNameAndAddress(jtable, record)
{
    var tableModel = jtable.getModel();
    for (var column = 0; column < testData.fieldNames(record).length; ++column) {
        var value = tableModel.getValueAt(0, column).toString();
        test.compare(value, testData.field(record, column));
    }
}
sub checkNameAndAddress
{
    my($jtable, $record) = @_;
    my $tableModel = $jtable->getModel();
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        my $value = $tableModel->getValueAt(0, $column);
        test::compare($value, testData::field($record, $column));
    }
}
def checkNameAndAddress(table, record)
    model = table.getModel()
    for column in 0...TestData.fieldNames(record).length
        value = model.getValueAt(0, column)
        Test.compare(value,  TestData.field(record, column))
    end
end
proc checkNameAndAddress {jtable record} {
    set tableModel [invoke $jtable getModel]
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set value [invoke $tableModel getValueAt 0 $column]
        test compare $value [testData field $record $column]
    }
}

此函数访问JTable的底层TableModel并提取每个单元格的值。然后我们使用Squish的test.compare-function函数来检查单元格中的值是否与我们使用的测试数据中的值相同。这个特定的AUT总是在当前行之前添加新行(如果没有行,则作为第一行),并且总是将添加的行设置为当前行。这种效果是,每个新的名字和地址总是会作为第一行添加,这就是为什么我们将行 cố định để là 0。

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

"Squish after a successful data-driven test run"

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

了解更多信息

现在我们已经完成了教程。Squish可以做的比这里展示的要多得多,但是目标是以尽可能快、尽可能容易的方式开始基本测试。《如何创建测试脚本》和《如何测试应用程序 - 具体细节》部分提供了更多示例,包括那些显示测试如何与特定的输入元素交互的示例,例如选择、选择一个、文本和文本区域。

API参考工具参考提供了Squish的测试API的完整细节以及它提供的众多功能,以便使测试尽可能简单和高效。阅读《如何创建测试脚本》和《如何测试应用程序 - 具体细节》,以及浏览API参考工具参考都是很有价值的。你所投入的时间都将得到回报,因为你会知道Squish提供了哪些开箱即用的功能,并能避免重新发明那些已经可以使用的功能。

以下列出了关键的Java AWT/Swing示例,以及它们使用的链接。

除了上述文档化的示例之外,Java AWT/Swing的其他示例应用程序和它们相应的测试在SQUISHDIR/examples/java中提供。

版权所有2024 The Qt Company Ltd. 以下文档的贡献者为各自版权所有者。
提供的文档是根据由自由软件基金会发布的GNU自由文档许可版1.3的条款许可的。
Qt及其相关标志是芬兰的The Qt Company Ltd.及其他国家/地区的注册商标。所有其他商标均为各自所有者的财产。