Java (SWT) Squish教程

学习如何测试基于SWT的应用程序。

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

教程:开始测试Java SWT应用程序

Squish 包含了一个集成开发环境和命令行工具。使用 squishide 是最简单和最佳的方式开始,但一旦你创建了大量的测试,你就会想自动化它们。例如,为了进行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或shell脚本中运行的命令行工具是有价值的。

注意:如果您需要一些视频指导,可以在 Qt Academy 上找到有关 Squish 基本用法的一个 45 分钟的在线课程。

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

截图显示了用户正在添加新名称和地址的操作中的应用程序。

"The Java SWT \c {addressbook_swt} example"

应用程序源代码、打包的 AddressBookSWT.jar 以及用于启动它的 .bat 文件都可以在 Squish 的示例中找到,位于 SQUISHDIR/examples/java/addressbook_swt/。我们将讨论的测试在子文件夹中,例如,使用 Python 编写的测试版本在 SQUISHDIR/examples/java/addressbook_swt/suite_py 中,而用其他语言编写的测试在类似命名的子文件夹中。

注意:在整个手册中,我们经常提到 SQUISHDIR 目录。这意味着 Squish 安装的目录,可能是 C:\Squish/usr/local/squish/opt/local/squish,或者某处,具体取决于您安装的位置。确切的地址不重要,只要您将 SQUISHDIR 目录在心里翻译成您在此手册中看到的路径和文件名所对应的实际目录。

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

测试Java AWT/Swing应用程序与Java SWT应用程序之间的重要不同之处在于:对于SWT,我们必须确保Java知道哪里可以找到swt.jar文件。这可以通过将该文件添加到CLASSPATH环境变量中轻松实现。(参见Java AUT类名和类路径。)或者,创建一个设置环境的正确脚本(shell脚本或Windows批处理文件)同样简单。第三种选择是将swt.jar的内容与您的应用程序包装在单个.jar中,就像我们的AddressBookSWT示例那样打包。

另一个重要的不同之处是由swt.jar针对特定架构和字长构建所导致的。因此,无论您使用哪种方法指向或包含AUT的swt.jar,请确保其架构和字长与Squish和JRE匹配。不匹配将导致Squish在启动或附加到AUT时出现问题。

包含在Squish中的我们的AddressBookSWT.jar示例AUT已被用于此教程,并包含了所有必要的组件,可以直接使用,无需进一步调整。

使用示例

对于Java AUT,您可以使用一个包含具有public static void main()类的.jar文件、一个包含main()函数的.class文件、一个批处理文件或一个shell脚本。一些可执行启动器也行。

当您第一次尝试为某个示例AUT运行测试时,您可能会收到一个错误,错误信息开始于“Squish找不到要启动的AUT”。如果发生此错误,则单击测试套件设置工具栏按钮,然后在正在测试的应用程序(AUT)部分从组合框中选择AUT(如果有),或者单击浏览按钮,并通过弹出文件打开对话框导航到AUT。这只需要为每个示例AUT执行一次。(当测试您自己的AUT时不会出现此情况。)

在macOS上测试

为了在测试Java SWT AUT时让Squish在macOS上正常工作,可以将环境变量SQUISH_USE_SWT设置为1,这样所需的Java VM选项-XstartOnFirstThread就被设置了。(最近的Eclipse/RCP不需要此选项,此选项已经在eclipse.ini中设置了。)可以通过以下方式完成:

export SQUISH_USE_SWT=1
open SQUISHDIR/squishide.app

可能最方便的方法是在您的登录shell脚本中设置环境变量或创建一个shell脚本,在运行Squish之前设置变量。

Squish概念

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

要进行测试,您需要

  1. 一个应用程序以进行测试,称为正在测试的应用程序(AUT)
  2. 一个测试脚本,用于执行AUT。

Squish的方法的一个基本方面是,受测软件(AUT)和测试脚本始终在两个独立的过程中执行。这确保了即使在AUT崩溃的情况下,也应该不会使Squish崩溃。在这种情况下,测试脚本将优雅地失败并记录错误消息。除了将Squish和测试脚本从AUT崩溃中隔离开来,运行AUT和测试脚本的独立进程也带来了其他好处。例如,它使得将测试脚本存储在中央位置并进行远程测试变得更加容易,在不同机器和平台上进行远程测试。远程测试的能力对于在多个平台上运行的AUT的测试以及运行在嵌入式设备上的AUT的测试特别有用。

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

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

以下图表说明了各个Squish工具是如何协同工作的。

"Squish tools"

从测试工程师的角度来看,这种分离不会引起注意,因为所有通信都已经在幕后透明地处理。

可以使用 squishide 编写和执行测试,在这种情况下,squishserver会自动启动和停止,测试结果将在 squishide测试结果视图 中显示。以下图表说明了在使用 squishide 时幕后发生了什么。

"Squish IDE"

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

注意: Squish文档主要使用术语 widget 来指代GUI对象,例如按钮、菜单、菜单项、标签和表格控件。Windows用户可能更熟悉 controlcontainer 术语,但在这里我们使用widget术语来指代这两者。类似地,macOS用户可能习惯于术语 view

使应用程序可测试

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

创建测试套件

测试套件是一组一个或多个测试案例(测试)。使用测试套件很方便,因为它使得在相关测试组之间共享脚本和测试数据变得更加容易。

在这里,以及在整个教程中,我们将首先描述如何使用 squishide 来做事情,然后会为命令行用户提供信息。

squishide 创建测试套件

启动 squishide,通过单击或双击squishide图标,从任务栏菜单启动squishide,或者通过命令行执行squishide,您可以选择您喜欢的方式并找适合您所使用平台的执行方法。当Squish启动后,您可能会看到一个欢迎页面。点击上右角的工作台按钮关闭它。之后,squishide的界面将类似于下面的截图,但根据您使用的窗口系统、色值、字体和主题,可能会有细微差别。

"The Squish IDE with no Test Suites"

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

"Name & Directory page"

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

"The New Test Suite wizard's Toolkit page"

如果您看到此向导页面,请选择您的应用程序使用的工具箱。对于这个示例,我们必须点击Java,因为我们正在测试一个Java应用程序——无论应用程序是基于SWT还是AWT/Swing,Java选项都涵盖两者。然后点击下一步进入脚本语言页面。

注意:Squish支持多种不同的脚本语言,不同的安装可能包括对其中一些或所有这些语言的支持——因此,截图中的脚本语言可能与您的Squish版本显示的不同。

"Scripting Language page"

选择您想要的任何脚本语言——唯一的要求是每个测试套件只能使用一种脚本语言。(因此,如果您想使用多种脚本语言,只需创建多个测试套件,每个测试套件对应您想使用的每种脚本语言。)Squish为所有语言提供了相同的功能。选定了脚本语言后,再次点击下一步以到达向导的最后页面。

"AUT page"

如果您正为Squish已知的应用程序创建新的测试套件,只需点击下拉框显示应用程序列表并选择您想要的。如果下拉框为空或您的应用程序未列出,点击下拉框右侧的浏览按钮,导航到应用程序:SQUISHDIR/examples/java/addressbook_swt/AddressBookSWT.jar。一旦选择了应用程序,点击完成,Squish将创建一个与测试套件同名的子文件夹,并在其中创建一个名为suite.conf的文件,其中包含测试套件的配置详情。Squish还会将应用程序注册到squishserver。然后向导将关闭,squishide将类似于下面的截图。

"The suite_py test suite"

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

从命令行创建测试套件

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

  1. 创建一个用于存放测试套件的目录——目录的名字应从 suite 开头。在本例中,我们创建了 SQUISHDIR/examples/java/addressbook_swt/suite_py 目录以存放 Python 测试。请注意,这里虽然也展示了其他语言的子目录,但这只是为了举例说明,因为我们通常只使用一种语言进行所有测试。
  2. 使用 squishserver 注册自动测试对象 (AUT)。

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

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

    squishserver --config addAUT AddressBookSWT.jar \
    SQUISHDIR/examples/java/addressbook_swt

    当然,Windows 用户会使用反斜杠(\\)而不是斜杠(/)作为目录分隔符。

    必须给 addAUT 命令提供 AUT 可执行文件的名字,以及—分开放—AUT 的路径。在本例中,路径是对应于测试套件配置文件中添加为 AUT 的 .jar 文件。有关应用程序路径的更多信息,请参阅 AUTs and Settings

  3. 在子目录中创建一个名为 suite.conf 的纯文本文件(ASCII 或 UTF-8 编码)。这是测试套件的配置文件,它至少必须标识 AUT、用于测试的脚本语言以及 AUT 使用的包装器(即,GUI 工具包或库)。文件的格式是 key = value,每行一个 key–value 对。例如
    AUT            = AddressBookSWT.jar
    LANGUAGE       = Python
    WRAPPERS       = Java
    OBJECTMAPSTYLE = script

    Java 程序的 AUT 是包含 AUT 类并能通过双击运行的 .jar 文件。LANGUAGE 可以设置为任何您偏好的语言一一目前 Squish 能够支持 JavaScript、Python、Perl、Ruby 和 Tcl,但具体可用性可能因 Squish 的安装方式而异。对于 Java AWT/Swing 和 Java SWT 程序,将 WRAPPERS 设置为 Java 就足够了。

我们现在可以开始记录我们的第一个测试了。

记录测试和验证点

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_swt/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. 点击第二行,然后点击编辑 > 添加
  3. 将弹出一个对话框,您需要输入“Jane”,“Doe”,“[email protected]”和一个电话号码“555 123 4567”。

    注意:请不要使用剪贴板操作(复制/粘贴)从另一个窗口输入数据;剪贴板内容不会记录在您的脚本中。

    无需担心输入错误——只需按退格键删除并正常修正。

  4. 单击确定按钮。现在应该有一个新的地址,其中包含您输入的详细信息。
  5. 单击第四行的第二列(姓氏)列,删除其文本并用“Doe”替换。然后按Enter键确认您的编辑。
  6. 现在单击第一行,然后点击编辑 > 删除,然后在消息框中点击按钮。第一行应该不见了,所以您的“Jane Doe”条目现在应该是第一个。
  7. 在Squish的控制栏窗口中单击验证工具栏按钮(从左数第二个按钮)并选择属性。这将使squishide出现。

  8. 应用程序对象视图中,展开地址簿对象,然后是表格对象。现在展开row_0对象。单击column_0对象使其属性出现在属性视图中,然后检查text属性的复选框。
  9. 现在单击column_1对象并检查其text属性。

    "Two verification points about to be inserted"

  10. 点击底部验证点创建器视图中的保存并插入验证按钮,将第一行的名字和姓氏验证插入到录制的测试脚本中。(参见下面的截图。)验证点插入后,squishide的窗口将再次隐藏,控制栏窗口和AUT将再次可见。
  11. 现在测试已完成,因此请在AUT中点击文件 > 退出,然后在消息框中点击,因为我们不希望保存任何更改。

退出AUT后,录制的测试将出现在squishide中,如图所示。(请注意,记录的确切代码将根据您如何交互而异。例如,您可能通过单击它们或使用按键序列来调用菜单选项——无论您使用哪种方法都没有关系,但由于它们不同,Squish将以不同的方式记录。)

"The recorded tst_general test"

如果录制的测试没有出现,请点击(根据您的平台和设置,可能是单击或双击)tst_general测试用例;这将使Squish在编辑器窗口中显示测试的test.py文件,如图所示。

SWT特有的chooseFile(objectOrName, filename)函数接受一个文件名,就像用户与标准窗口系统提供的文件打开对话框交互一样。Squish记录文件的绝对路径,以确保在运行测试时的可靠性,但在本教程中,我们使用了脚本语言功能,提供了一个在任何示例安装位置的文件名。

记录了测试后,我们能够将其回放,即运行它。本身就是有用的,因为如果回放失败,这可能意味着应用程序已被破坏。此外,我们放置的两个验证将在回放时检查,如图像所示。

在测试记录过程中插入验证点非常方便。在这里,我们一次性插入了两个,但我们可以在测试记录过程中根据需要插入任意多个验证点。然而,有时我们可能会忘记插入验证,或者后来我们可能会想插入新的验证。我们可以在下一节中轻松地将额外的验证插入到已记录的测试脚本中,我们将看到这一点,插入额外的验证点

在进一步操作之前,我们将了解如何从命令行记录测试。然后我们将看到如何运行测试,我们还将查看一些Squish生成的记录测试的代码,并讨论它的某些特性。

从命令行记录测试

在记录测试时,squishserver必须始终在运行状态。这由squishide自动处理,但命令行用户必须根据指示手动启动squishserver,如squishserver中所述。

要从命令行记录测试,请执行squishrunner程序,并指定要记录的测试套件名称和测试用例名称。例如,假设我们处于包含测试套件目录的目录中

squishrunner --testsuite suite_py --record tst_general --useWaitFor

最好使用--useWaitFor选项来记录对Object waitForObject(objectOrName)函数的调用,尽管由于历史原因默认使用此选项,但它比使用snooze(seconds)函数更可靠。squishide自动使用Object waitForObject(objectOrName)函数。

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

从IDE运行测试

要在squishide中运行测试用例,请单击在测试套件视图中悬停或选择测试用例时出现的运行测试)。

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

从命令行运行测试

运行测试时,squishserver必须始终在运行状态,或者必须向squishrunner提供--local选项。有关更多信息,请参阅squishserver

要回放从命令行记录的测试,我们执行squishrunner程序,并指定我们的记录脚本所在的测试套件和我们要播放的测试用例。例如,假设我们正在包含测试套件目录的目录中

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

在某些情况下,运行测试可能会因类似于以下错误信息的错误而失败

ERROR       Mon Mar 23 15:41:12 2009    test.py:31: Script Error
    Error in closeWindow() invocation: null object

这有时可能是因为对话框被匆忙地调用dispose()。只需进入有问题的行,并取消注释closeWindow()调用(在这个例子中位于第18行),然后重新运行测试。现在测试应该能正常工作。

检查生成的代码

如果您查看屏幕截图中的代码(或下文中显示的代码片段),您将看到很多作为其他各种调用(如Object waitForObject(objectOrName)),activateItem(objectOrName, itemText)clickButton(objectOrName),以及type(objectOrName, text)参数的Object waitForObject(objectOrName)调用。该Object waitForObject(objectOrName)函数会等待直到一个GUI对象可以被交互使用(即可见且可用),然后紧跟着是一些与该对象交互的函数。典型的交互包括激活(弹出)菜单,点击菜单选项或按钮,或输入一些文本。

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

对象由Squish生成的名称标识。有关详细信息,请参阅如何识别和访问对象.

生成的代码大约有30行。以下是一个摘录,仅显示Squish如何记录点击编辑菜单的添加选项,将Jane Doe的详细信息输入到添加对话框中,并在最后点击确定以关闭对话框并更新表格。

注意:尽管屏幕截图仅显示Python测试套件在运行,但对于这里引用的代码片段和整个教程,我们显示了Squish支持的所有脚本语言的代码。实际上,您通常会使用其中一种,因此请放心,您只需查看您感兴趣的代码片段,并跳过其他语言。

    activateItem(waitForObjectItem(names.address_Book_MyAddresses_adr_Menu, "Edit"))
    activateItem(waitForObjectItem(names.edit_Menu, "Add..."))
    type(waitForObject(names.address_Book_Add_Forename_Text), "Jane")
    type(waitForObject(names.address_Book_Add_Surname_Text), "Doe")
    type(waitForObject(names.address_Book_Add_Email_Text), "[email protected]")
    type(waitForObject(names.address_Book_Add_Phone_Text), "123 555 1212")
    clickButton(waitForObject(names.address_Book_Add_OK_Button))
    activateItem(waitForObjectItem(names.addressBookMyAddressesAdrMenu, "Edit"));
    activateItem(waitForObjectItem(names.editMenu, "Add..."));
    type(waitForObject(names.addressBookAddForenameText), "Jane");
    type(waitForObject(names.addressBookAddSurnameText), "Doe");
    type(waitForObject(names.addressBookAddEmailText), "[email protected]");
    type(waitForObject(names.addressBookAddPhoneText), "123 555 1212");
    clickButton(waitForObject(names.addressBookAddOKButton));
    activateItem(waitForObjectItem($Names::address_book_myaddresses_adr_menu, "Edit"));
    activateItem(waitForObjectItem($Names::edit_menu, "Add..."));
    type(waitForObject($Names::address_book_add_forename_text), "Jane");
    type(waitForObject($Names::address_book_add_surname_text), "Doe");
    type(waitForObject($Names::address_book_add_email_text), "jane.doe\@nowhere.com");
    type(waitForObject($Names::address_book_add_phone_text), "123 555 1212");
    clickButton(waitForObject($Names::address_book_add_ok_button));
    activateItem(waitForObjectItem(Names::Address_Book_MyAddresses_adr_Menu, "Edit"))
    activateItem(waitForObjectItem(Names::Edit_Menu, "Add..."))
    type(waitForObject(Names::Address_Book_Add_Forename_Text), "Jane")
    type(waitForObject(Names::Address_Book_Add_Surname_Text), "Doe")
    type(waitForObject(Names::Address_Book_Add_Email_Text), "[email protected]")
    type(waitForObject(Names::Address_Book_Add_Phone_Text), "123 555 1212")
    clickButton(waitForObject(Names::Address_Book_Add_OK_Button))
    invoke activateItem [waitForObjectItem $names::Address_Book_MyAddresses_adr_Menu "Edit"]
    invoke activateItem [waitForObjectItem $names::Edit_Menu "Add..."]
    invoke type [waitForObject $names::Address_Book_Add_Forename_Text] "Jane"
    invoke type [waitForObject $names::Address_Book_Add_Surname_Text] "Doe"
    invoke type [waitForObject $names::Address_Book_Add_Email_Text] "[email protected]"
    invoke type [waitForObject $names::Address_Book_Add_Phone_Text] "123 555 1212"
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button]

在记录过程中,测试员使用键盘从文本字段切换到另一个文本字段,但我们为了简洁性移除了它们,因为它们不是必需的。如果测试员通过点击鼠标移动焦点,并通过Tab键将其移动到OK按钮并按空格或任何其他交互组合来点击按钮,结果将是相同的,但当然Squish会记录实际发生的动作。

请注意,代码片段中没有明确的延迟。(可以使用Squish的snooze(seconds)函数强制延迟。)这是因为Object waitForObject(objectOrName)函数会延迟直到它给出的对象准备好——从而允许Squish以GUI工具包能够处理的最大速度运行,但不会更快。

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

当符号名称位于光标下时,编辑器的上下文菜单允许您 打开符号名称,显示其在 对象映射 中的条目,或者 转换为实际名称,这将在您的脚本语言中在光标处插入内联映射,允许您在脚本中直接编辑属性。

现在我们已看到如何记录和播放测试及看到了 Squish 生成的代码,让我们更进一步,确保测试执行中特定点满足某些条件。

插入附加验证点

在上一个部分中,我们看到了在录制测试脚本过程中插入检查点有多么容易。您还可以通过设置断点并使用 squishide,或者在测试脚本中编辑并调用 Squish 的测试函数(如 布尔值 test.compare(value1, value2)布尔值 test.verify(condition))将检查点插入到现有的测试脚本中。

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

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

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

在使用Squish插入验证点之前,最好确保我们已经列出我们想要验证的内容以及何时进行验证。我们可以在测试用例中添加许多潜在的验证,但既然我们在这里只关心展示如何操作,我们只会做两步——我们将验证“Jane Doe”条目的电子邮件地址和电话号码是否与输入的一致,并将这些验证放置在我们的录制中插入的验证之后。

要使用squishide插入验证点,我们首先需要在脚本中放置一个断点(无论是录制的还是手动编写的——对Squish来说都不重要),在想要验证的位置。

"The tst_general test case with a breakpoint"

如上面的截图所示,我们在第27行设置了断点。这可以通过双击,或者在编辑器左侧的空白区域(行号旁边)右击并选择添加断点菜单项来完成。我们选择这一行是因为它紧接着第一条地址被删除的脚本行,因此在这一点(在调用文件菜单关闭应用程序之前),第一条地址应该是“Jane Doe”的。截图显示了在录制期间使用squishide输入的验证。我们的附加验证将跟在这些验证之后。(请注意,如果你的行号与你以不同方式录制的测试不同,例如,使用键盘快捷键而不是点击菜单项,那么你的行号可能会不同。)

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

视角与视图

squishide的工作方式与Eclipse IDE类似。如果你不熟悉Eclipse,以下关键概念至关重要:视图视角。在Eclipse中,以及因此也在squishide中,一个视图本质上是一个子窗口,如停靠窗口或现有窗口中的选项卡。一个视角是一组排列在一起的视图。这两个都可以通过窗口菜单访问。

squishide附带以下视角

您可以更改这些视角以显示更多视图或隐藏不需要的视图,或者创建自己的视角,其中包括您确切想要的视图。所以如果你的窗口发生了很大变化,只是意味着视角发生了变化。使用窗口菜单返回您想要的视角。然而,Squish会自动更改视角以反映当前情况,因此您通常不需要手动更改视角。

插入验证点

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

要插入一个验证点,我们可以在应用程序对象视图中展开项目,直到找到我们想要验证的对象。在这个例子中,我们想验证表格第一行的文本,所以我们展开Address Book项,然后展开它的子项,直到找到Table,然后在该对象中找到我们感兴趣的行和列。一旦我们点击列对象,它的属性会如图所示显示在属性视图中。

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

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

在这里,我们可以看到第1行第0列的项目中的text属性值为"Jane";我们已经在这段录音中插入了一个验证。向下滚动,以便可以看到第1行第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的测试函数,比如test.compare-functiontest.verify-function。在实践中,最好确保Squish首先知道我们想要验证的对象,这样它可以在测试运行时找到它们。这涉及到与使用squishide插入相似的程序。首先,我们设置一个打算添加验证的断点。然后,运行测试脚本直到它停止。接下来,我们在应用程序对象视图中导航,直到找到我们想要验证的对象。在这个时候,右键单击我们感兴趣的物体并单击上下文菜单中的添加到对象映射选项是明智的。这将确保Squish可以访问该对象。然后再次右键单击并单击上下文菜单中的复制符号名选项——这给了我们Squish用于识别该对象的名称。现在我们可以编辑测试脚本以添加我们自己的验证并完成或停止执行。(别忘了在不需要时就禁用断点。)

尽管我们可以编写与我们自动生成代码相同风格的测试脚本代码,但通常以略微不同的风格操作更清晰、更容易,我们将在下面解释。

对于我们手工进行的验证,我们希望在读取MyAddresses.adr文件后,检查表中存在的地址数量,然后在新地址添加后,最后在第一个地址被删除后。屏幕截图显示了为了获取这三种验证中的一种我们输入的两行代码,以及测试脚本运行的结果。

"Manually entered verification points"

当手工编写脚本时,我们使用Squish的test模块的函数来验证测试脚本执行期间特定点的条件。如图所示(和以下代码段),我们首先获取对我们要关注的对象的引用。使用Object waitForObject(objectOrName)函数是手动编写测试脚本的标准做法。该函数等待对象可用(即可见和启用),然后返回对其的引用。(否则它会超时并引发可捕获的异常。)然后我们使用这个引用来访问项的属性——在本例中为表的行数——并使用布尔test.verify(condition)函数来验证值是否如我们期望。顺便提一下,我们得到了对象名称来自上一行,因此我们不需要设置断点并手动将表的名称添加到对象映射中,以确保Squish会记住这个特定的情况,因为在测试录制期间Squish已经添加了它。)

以下是手动输入的代码,用于对Squish所支持的所有脚本语言进行首次验证。自然,你只需查看你将要用于自己测试的语言的代码。

    table = waitForObject(names.address_Book_MyAddresses_adr_Table)
    test.compare(table.getItemCount(), 125)
    var table = waitForObject(names.addressBookMyAddressesAdrTable);
    test.compare(table.getItemCount(), 125);
    my $table = waitForObject($Names::address_book_myaddresses_adr_table);
    test::compare($table->getItemCount(), 125);
    table = waitForObject(Names::Address_Book_MyAddresses_adr_Table)
    Test.compare(table.getItemCount(), 125)
    set table [waitForObject $names::Address_Book_MyAddresses_adr_Table]
    test compare [invoke $table getItemCount] 125

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

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

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

测试结果

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

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

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

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

手动创建测试

在看到如何录制测试并插入验证点以修改它之后,我们现在准备了解如何手动创建测试。做这件事的最简单方法是修改和重构录制的测试,尽管从头开始创建手动测试也是完全可能的。

编写手动测试中最具挑战性的部分可能是使用正确的对象名称,但在实践中,这很少成为问题。我们可以复制Squish在记录上次测试时已添加到对象图的符号名称,或者直接从记录的测试中复制对象名称。如果我们还没有记录任何测试并从零开始,可以使用Spy。我们通过点击启动AUT工具栏按钮来这样做。这会启动AUT并将其切换到Spy视角。然后我们可以与AUT交互,直到我们感兴趣的对象可见。然后,在squishide内部,我们可以导航到应用程序对象视图中的对象,并使用上下文菜单将该对象添加到对象图(这样Squish会记住它)以及到剪贴板(这样我们就可以在我们的测试脚本中粘贴它)。最后,我们可以单击退出AUT工具栏按钮来终止AUT并返回到测试管理视角。请参阅如何使用Spy以获取有关使用Spy的更多详细信息。

我们可以通过单击对象图工具栏按钮来查看对象图(另请参阅对象图视图)。Squish与之交互的每个应用程序对象都列在这里,无论是作为顶级对象还是作为子对象(视图是一种树形视图)。我们可以通过右键单击我们感兴趣的对象,然后单击上下文菜单中的复制项来检索Squish在记录的脚本中使用的符号名称。这在我们要修改现有的测试脚本或我们要从头开始创建测试脚本时很有用,正如我们将在教程中稍后看到的那样。

"Squish Object Map"

修改和重构录制的测试

假设我们想要测试AUT的添加功能,添加三个新的名称和地址。我们可以当然记录这样的测试,但使用代码做同样的事情也同样简单。我们需要测试脚本执行的步骤是,单击文件 > 新建来创建一个新的地址簿,然后,对每个新的名称和地址

  1. 选择编辑 > 添加
  2. 填写联系详细信息。
  3. 单击确定

最后,不保存更改地单击文件 > 退出

我们还希望在开始时验证没有数据行,结束时有三行。我们还将一边做一边重构,使我们的代码尽可能整洁和模块化。

首先我们必须创建一个新的脚本测试用例。点击文件 > 新建测试用例并将测试用例的名称设置为tst_adding。Squish将自动创建一个test.py(或test.js等)文件。

命令行用户只需在测试套件的目录内创建一个名为tst_adding的新目录,并在该目录内创建和编辑test.py文件(或test.js等)。

我们需要的第一件事是启动AUT并调用菜单选项的方法。以下是来自recorded tst_general脚本的最初几行

import names
import os

def main():
    fn = os.path.join(os.getenv("SQUISH_PREFIX"),
        "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"{}"'.format(fn))
    activateItem(waitForObjectItem(names.address_Book_Menu, "File"))
    activateItem(waitForObjectItem(names.file_Menu, "Open..."))
import * as names from 'names.js';

function main() {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook_swt/AddressBookSWT.jar"');
    activateItem(waitForObjectItem(names.addressBookMenu, "File"));
    activateItem(waitForObjectItem(names.fileMenu, "Open..."));
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook_swt/AddressBookSWT.jar\"");
    activateItem(waitForObjectItem($Names::address_book_menu, "File"));
    activateItem(waitForObjectItem($Names::file_menu, "Open..."));
require 'squish'
require 'names'

include Squish

def main
    fn = File.join(ENV['SQUISH_PREFIX'], "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"' + fn + '"');
    activateItem(waitForObjectItem(Names::Address_Book_Menu, "File"))
    activateItem(waitForObjectItem(Names::File_Menu, "Open..."))
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/addressbook_swt/AddressBookSWT.jar\""
    invoke activateItem [waitForObjectItem $names::Address_Book_Menu "File"]
    invoke activateItem [waitForObjectItem $names::File_Menu "Open..."]

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

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

如果在测试执行过程中,自动化测试工具(AUT)似乎冻结了,请等待Squish超时AUT(大约20秒),并显示对象未找到对话框,指示如下错误

"Object Not Found dialog"

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

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

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

我们现在几乎准备好编写自己的测试脚本了。最简单的开始方式是录制一个虚拟测试。因此,请点击文件 > 新建测试用例,将测试用例的名称设置为tst_dummy。然后点击虚拟测试用例的录制测试用例工具栏按钮()。一旦AUT启动,点击文件 > 新建,然后点击(空)表格,然后点击编辑 > 添加,添加一个条目,然后按Return或点击确定。最后,点击文件 > 退出完成,并拒绝保存更改。然后只重新播放此测试以确认一切正常。这样做的主要目的是确保Squish向对象图中添加必要的名称,因为这样可以更快地完成,而不必在每一个感兴趣的表对象上使用Spy。在重新播放虚拟测试后,可以根据需要删除它。

在对象图中有我们需要的所有对象名称后,我们现在可以完全从头开始编写自己的测试脚本。我们将从main函数开始,然后我们将查看main函数所使用的辅助函数。

import names
import os

def main():
    fn = os.path.join(os.getenv("SQUISH_PREFIX"), "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"{}"'.format(fn));
    invokeMenuItem("File", "New...")
    table = waitForObject({"type": "org.eclipse.swt.widgets.Table"})
    test.compare(table.getItemCount(), 0)
    data = [("Andy", "Beach", "[email protected]", "555 123 6786"),
            ("Candy", "Deane", "[email protected]", "555 234 8765"),
            ("Ed", "Fernleaf", "[email protected]", "555 876 4654")]
    for details in data:
        addNameAndAddress(details)
    test.compare(table.getItemCount(), 3)
    closeWithoutSaving()
import * as names from 'names.js';

function main() {
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook_swt/AddressBookSWT.jar"');
    invokeMenuItem("File", "New...");
    var table = waitForObject({"type": "org.eclipse.swt.widgets.Table"});
    test.verify(table.itemcount == 0);
    var data = [
    ["Andy", "Beach", "[email protected]", "555 123 6786"],
    ["Candy", "Deane", "[email protected]", "555 234 8765"],
    ["Ed", "Fernleaf", "[email protected]", "555 876 4654"]];
    for (var row = 0; row < data.length; ++row)
        addNameAndAddress(data[row]);
    test.compare(table.itemcount, 3);
    closeWithoutSaving();
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook_swt/AddressBookSWT.jar\"");
    invokeMenuItem("File", "New...");
    my $table = waitForObject({"type" => "org.eclipse.swt.widgets.Table"});
    test::compare($table->getItemCount(), 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($table->getItemCount(), 3);
    closeWithoutSaving();
}
require 'squish'
require 'names'
include Squish

def main
    fn = File.join(ENV['SQUISH_PREFIX'], "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"' + fn + '"');
    invokeMenuItem("File", "New...")
    table = waitForObject({:isvisible => true, :type => "org.eclipse.swt.widgets.Table"})
    Test.verify(table.getItemCount() == 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.getItemCount(), data.length)
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/addressbook_swt/AddressBookSWT.jar\""
    invokeMenuItem "File" "New..."
    set table [waitForObject [::Squish::ObjectName type org.eclipse.swt.widgets.Table]]
    test compare [invoke $table getItemCount] 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 [invoke $table getItemCount] 3
    closeWithoutSaving
}

我们的第一步是调用 ApplicationContext startApplication(autName) 函数来启动应用程序。我们传递给函数的字符串是我们已经注册在 Squish 中的名称(通常是指含应用程序代码的 .jar 文件或含有 main 方法的类的 .class 文件)。Object waitForObject(objectOrName) 函数会等待一个对象准备好(可见且启用),并返回对该对象的引用——或者它超时并引发可捕获的异常。我们使用了一个 真实名称 来访问表格。这是一个将属性映射到值的键值映射,我们通过选择并右键单击一个符号名称,然后调用 将转换成真实名称)来获取它。然后我们移除了在此情况下不相关的属性。《code translate="no">table 变量可以用来访问 Table 中的任何公共方法和属性。

我们特别创建了一个名为 invokeMenuItem 的函数来应对这次测试。它接受一个菜单名称和一个菜单选项名称,并调用该菜单选项。使用 invokeMenuItem 函数执行 文件 > 新建 后,我们验证表格的行数是 0。《a href="squish-api.html#test-verify-function" translate="no">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({"menuStyle": "SWT.BAR", "type": "org.eclipse.swt.widgets.Menu"}, menu))
    mouseClick(waitForObjectItem({"caption": menu, "menuStyle": "SWT.DROP_DOWN", "type": "org.eclipse.swt.widgets.Menu"}, item))
function invokeMenuItem(menu, item)
{
    activateItem(waitForObjectItem({"menuStyle": "SWT.BAR", "type": "org.eclipse.swt.widgets.Menu"}, menu));
    activateItem(waitForObjectItem({"caption": menu, "menuStyle": "SWT.DROP_DOWN", "type": "org.eclipse.swt.widgets.Menu"}, item));
}
sub invokeMenuItem
{
    my($menu, $item) = @_;
    activateItem(waitForObjectItem({"menuStyle" => "SWT.BAR", "type" => "org.eclipse.swt.widgets.Menu"}, $menu));
    activateItem(waitForObjectItem({"caption" => $menu, "type" => "org.eclipse.swt.widgets.Menu"} , $item));
}
def invokeMenuItem(menu, item)
    activateItem(waitForObjectItem(
        {:menuStyle => "SWT.BAR", :type => "org.eclipse.swt.widgets.Menu"}, menu))
    activateItem(waitForObjectItem(
        {:caption => menu, :type => "org.eclipse.swt.widgets.Menu"}, item))
end
proc invokeMenuItem {menu item} {
    invoke activateItem [waitForObjectItem [::Squish::ObjectName menuStyle SWT.BAR type org.eclipse.swt.widgets.Menu] $menu]
    invoke activateItem [waitForObjectItem [::Squish::ObjectName caption $menu type org.eclipse.swt.widgets.Menu] $item]
}

如我们前面所述,Squish 对菜单、菜单项(以及其他对象)使用的符号名称可能因上下文而异,通常名称的起始部分来自窗口标题。对于在标题中放置当前文件名的应用程序(如地址簿示例),名称将包含文件名,我们必须考虑这一点。

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

符号名称是带有 names 前缀的脚本变量。变量名称内嵌有关对象及其类型的各种信息。真实名称在脚本语言中由键值映射表示。每个真实名称都必须指定类型属性,通常至少还指定其他一个属性。在这里,我们使用类型属性来唯一标识菜单栏,并使用类型和标题属性来唯一标识菜单。

一旦我们确定了要与之交互的对象,我们使用Object waitForObjectItem(objectOrName, itemOrIndex)函数来获取对该对象的引用,并在这种情况下对它应用mouseClick(screenPoint, modifierState, button)函数。Object waitForObjectItem(objectOrName, itemOrIndex)函数会暂停Squish,直到指定的对象及其项目可见并启用。因此,在此情况下,我们等待菜单栏及其一个菜单栏项目,然后等待菜单的下拉菜单项目。等待结束后,每次我们都会使用mouseClick(screenPoint, modifierState, button)函数激活该对象及其项目。

def addNameAndAddress(fields):
    invokeMenuItem("Edit", "Add...")
    type(waitForObject(names.address_Book_Add_Forename_Text), fields[0])
    type(waitForObject(names.address_Book_Add_Surname_Text), fields[1])
    type(waitForObject(names.address_Book_Add_Email_Text), fields[2])
    type(waitForObject(names.address_Book_Add_Phone_Text), fields[3])
    clickButton(waitForObject(names.address_Book_Add_OK_Button))
function addNameAndAddress(details)
{
    invokeMenuItem("Edit", "Add...");
    type(waitForObject(names.addressBookAddForenameText), details[0]);
    type(waitForObject(names.addressBookAddSurnameText), details[1]);
    type(waitForObject(names.addressBookAddEmailText), details[2]);
    type(waitForObject(names.addressBookAddPhoneText), details[3]);
    clickButton(waitForObject(names.addressBookAddOKButton));
    snooze(0.5);
}
sub addNameAndAddress
{
    my($details) = @_;
    invokeMenuItem("Edit", "Add...");
    type(waitForObject($Names::address_book_add_forename_text), $_[0]);
    type(waitForObject($Names::address_book_add_surname_text), $_[1]);
    type(waitForObject($Names::address_book_add_email_text), $_[2]);
    type(waitForObject($Names::address_book_add_phone_text), $_[3]);
    clickButton(waitForObject($Names::address_book_add_ok_button));
    snooze(0.5);
}
def addNameAndAddress(oneNameAndAddress)
    invokeMenuItem("Edit", "Add...")
    type(waitForObject(Names::Address_Book_Add_Forename_Text), oneNameAndAddress[0])
    type(waitForObject(Names::Address_Book_Add_Surname_Text), oneNameAndAddress[1])
    type(waitForObject(Names::Address_Book_Add_Email_Text), oneNameAndAddress[2])
    type(waitForObject(Names::Address_Book_Add_Phone_Text), oneNameAndAddress[3])
    clickButton(waitForObject(Names::Address_Book_Add_OK_Button))
    snooze(0.5)
end
proc addNameAndAddress {details} {
    invokeMenuItem "Edit" "Add..."
    invoke type [waitForObject $names::Address_Book_Add_Forename_Text] [lindex $details 0]
    invoke type [waitForObject $names::Address_Book_Add_Surname_Text] [lindex $details 1]
    invoke type [waitForObject $names::Address_Book_Add_Email_Text] [lindex $details 2]
    invoke type [waitForObject $names::Address_Book_Add_Phone_Text] [lindex $details 3]
    invoke clickButton [waitForObject $names::Address_Book_Add_OK_Button]
    snooze 0.5
}

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

def closeWithoutSaving():
    invokeMenuItem("File", "Quit")
    closeMessageBox(waitForObject(names.sWT), SWT.NO)
function closeWithoutSaving()
{
    invokeMenuItem("File", "Quit");
    closeMessageBox(waitForObject(names.sWT), SWT.NO);
}
sub closeWithoutSaving
{
    invokeMenuItem("File", "Quit");
    closeMessageBox(waitForObject($Names::swt), SWT->NO);
}
def closeWithoutSaving
    invokeMenuItem("File", "Quit")
    closeMessageBox(waitForObject(Names::SWT), SWT::NO)
end
proc closeWithoutSaving {} {
    invokeMenuItem "File" "Quit"
    invoke closeMessageBox [waitForObject $names::SWT] [enum SWT NO]
}

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

整个测试不到30行代码——如果我们将一些常用函数(如invokeMenuItemcloseWithoutSaving)放入共享脚本中,将会更少。其中大部分代码直接从记录的测试中复制,并在某些情况下进行了参数化。

这一点应该足以展示如何为AUT编写测试脚本。请记住,Squish提供的功能远不止我们所使用的(所有这些都包括在API参考工具参考中)。Squish还提供了访问AUT对象的全部公共API的机会。

然而,测试用例的一个方面不是很令人满意。尽管像我们在这里这样做将嵌入测试数据是有意义的,但是对于大量测试数据来说限制性较大。此外,我们没有测试添加的数据,以查看它是否正确地出现在了表中。在下文中,我们将创建这个测试的新版本,但这一次我们将从外部数据源获取数据,并检查表中的数据是否正确。

创建数据驱动测试

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

注意:假设.csv.tsv文件使用Unicode UTF-8编码,与所有测试脚本相同的编码。

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

对于addressbook_swt应用程序,我们想导入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():
    fn = os.path.join(os.getenv("SQUISH_PREFIX"), "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"{}"'.format(fn))
    invokeMenuItem("File", "New...")
    table = waitForObject({"type": "org.eclipse.swt.widgets.Table"})
    test.verify(table.getItemCount() == 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(table, record)
        if row > limit:
            break
    waitForObject(table)
    test.compare(table.getItemCount(), row + 1)
    closeWithoutSaving()
import * as names from 'names.js';

function main()
{
    startApplication('"' + OS.getenv("SQUISH_PREFIX") +
        '/examples/java/addressbook_swt/AddressBookSWT.jar"');
    invokeMenuItem("File", "New...");
    var table = waitForObject({"type": "org.eclipse.swt.widgets.Table"});
    test.verify(table.itemcount == 0);
    var limit = 10; // To avoid testing 100s of rows since that would be boring
    var records = testData.dataset("MyAddresses.tsv");
    var row = 0;
    for (; row < records.length; ++row) {
        var record = records[row];
        var forename = testData.field(record, "Forename");
        var surname = testData.field(record, "Surname");
        var email = testData.field(record, "Email");
        var phone = testData.field(record, "Phone");
        addNameAndAddress([forename, surname, email, phone]);
        checkNameAndAddress(table, record);
        if (row > limit)
            break;
    }
    test.compare(table.itemcount, row + 1);
    closeWithoutSaving();
}
require 'names.pl';

sub main
{
    startApplication("\"$ENV{'SQUISH_PREFIX'}/examples/java/addressbook_swt/AddressBookSWT.jar\"");
    invokeMenuItem("File", "New...");
    my $table = waitForObject({"type" => "org.eclipse.swt.widgets.Table"});
    test::compare($table->getItemCount(), 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($table, $record);
        if ($row > $limit) {
            last;
        }
    }
    test::compare($table->getItemCount(), $row + 1);
    closeWithoutSaving();
}
require 'names'
include Squish

def main
    fn = File.join(ENV['SQUISH_PREFIX'], "examples/java/addressbook_swt/AddressBookSWT.jar")
    startApplication('"' + fn + '"');
    invokeMenuItem("File", "New...")
    table = waitForObject({:type => "org.eclipse.swt.widgets.Table"})
    Test.verify(table.getItemCount() == 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(table, record)
        break if row > limit
        rows += 1
    end
    Test.compare(table.getItemCount(), rows + 1)
    closeWithoutSaving
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "\"$::env(SQUISH_PREFIX)/examples/java/addressbook_swt/AddressBookSWT.jar\""
    invokeMenuItem "File" "New..."
    set table [waitForObject [::Squish::ObjectName type org.eclipse.swt.widgets.Table]]
    test compare [invoke $table getItemCount] 0
    set limit 10
    set data [testData dataset "MyAddresses.tsv"]
    set columns [llength [testData fieldNames [lindex $data 0]]]
    set row 0
    for {} {$row < [llength $data]} {incr row} {
        set record [lindex $data $row]
        set forename [testData field $record "Forename"]
        set surname [testData field $record "Surname"]
        set email [testData field $record "Email"]
        set phone [testData field $record "Phone"]
        set details [list $forename $surname $email $phone]
        addNameAndAddress $details
        checkNameAndAddress $table $record
        if {$row > $limit} {
            break
        }
    }
    test compare [invoke $table getItemCount] [expr $row + 1]
    closeWithoutSaving
}

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

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

def checkNameAndAddress(table, record):
    item = table.getItem(0) # new rows are always inserted at row 0
    for column in range(len(testData.fieldNames(record))):
        value = item.getText(column)
        test.compare(value, testData.field(record, column))
function checkNameAndAddress(table, record)
{
    var item = table.getItem(0); // new rows are always inserted at row 0
    for (var column = 0; column < testData.fieldNames(record).length; ++column) {
        var value = item.getText(column);
        test.compare(value, testData.field(record, column));
    }
}
sub checkNameAndAddress
{
    my($table, $record) = @_;
    waitForObject($table);
    my $item = $table->getItem(0); # new rows are always inserted at row 0
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        my $value = $item->getText($column);
        test::compare($value, testData::field($record, $column));
    }
}
def checkNameAndAddress(table, record)
  item = table.getItem(0) # new rows are always inserted at row 0
  for column in 0...TestData.fieldNames(record).length
      Test.compare(item.getText(column), TestData.field(record, column))
  end
end
proc checkNameAndAddress {table record} {
    set item [invoke $table getItem 0]
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set value [invoke $item getText $column]
        test compare $value [testData field $record $column]
    }
}

此函数访问表格中的单个项。然后我们使用Squish的布尔测试.compare(value1, value2)函数来检查单元格中的值是否与我们使用的测试数据中的值相同。请注意,这个特殊的AUT总是在当前行之前添加新行(如果还没有行,则是第一行),并且总是将添加的行设置为当前行。这种效果是每次新的姓名和地址都被添加为第一行,这就是为什么我们将行硬编码为0。

此截图显示了运行数据驱动测试后的Squish测试摘要日志。

"Squish after a successful data-driven test run"

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

了解更多

现在我们已经完成了教程。Squish可以做的事情比我们这里展示的要多得多,但我们 aim has been to get you started with basic testing as quickly and easily as possible. 《如何创建测试脚本》和《如何测试应用程序 - 特定内容》部分提供了更多示例,包括显示测试如何与特定输入元素交互的示例,例如选择、单选选择、文本和文本区域。

API参考工具参考提供了关于Squish测试API的详细信息以及它提供的许多功能,以使测试尽可能简单和高效。阅读《如何创建测试脚本》和《如何测试应用程序 - 特定内容》,以及浏览《API参考》和《工具参考》是值得的。你投入的时间将得到回报,因为你将了解Squish提供的功能,并可以避免重造已经存在的东西。

下面列出了关键的Java SWT示例及其使用链接。

©2024 The Qt Company Ltd. 本文档中包含的贡献的版权属于各自的拥有者。
在此提供的文档是根据自由软件基金会出版的《GNU自由文档许可证》第1.3版下的条款获得许可的。
Qt 及其分别的标志是芬兰和/或世界其他国家的 Qt 公司有限公司的商标。所有其他商标均为其各自所有者的财产。