Qt Tk Squish教程

学习如何测试Tk应用程序。

教程:开始测试 Tk 应用程序

Squish 包含了一个 IDE 和命令行工具。使用 squishide 是开始的最简单和最佳方式,但一旦积累了大量测试,您就会想要自动化它们。例如,进行回归测试套件的夜间运行。因此,了解如何使用可以运行在批处理文件或 shell 脚本中的命令行工具是很重要的。

注意:如果您想得到一些视频指导,访问 Qt Academy 上的一个 45 分钟的 关于 Squish 基本使用的在线课程

我们使用一个简单的通讯录脚本作为我们的应用。它作为 SQUISHDIR/examples/tk/addressbook.tcl 随 Squish 一起提供。这是一个 Tcl/Tk 脚本,允许用户加载现有的通讯录或创建一个新的,添加、编辑和删除条目,以及保存(或另存为)新的或修改过的通讯录。尽管应用很简单,但它具有大多数标准应用都具有的所有关键功能:一个带有下拉菜单的菜单栏、工具栏和一个中心区域——在这种情况下显示一个表格。它弹出模态对话框来添加和编辑条目。您学习来测试此应用的所有想法和实践可以轻松地适应您自己的应用。有关测试 Tk 特定功能和标准编辑小部件的更多示例,请参见 如何创建测试脚本如何测试 Tk 应用程序

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

要执行 Tk 通讯录应用程序,您需要在您的系统上有一个可用的 Tcl/Tk 安装。

截图显示了用户添加新姓名和地址时应用程序的运行情况。

"The Tk addressbook.tcl example"

使用示例

当您第一次尝试运行示例 AUT 中的一个测试时,可能会出现一个以 Squish 找不到要启动的 AUT... 开头的致命错误。要从错误中恢复,请点击 测试套件设置 工具栏按钮,然后在 测试应用(AUT) 部分,如果可用,从组合框中选择 AUT,或者点击 浏览 按钮并在查找器对话框中导航到 AUT 的可执行文件。某些版本的 Squish 如果未指定 AUT,则自动打开此对话框。您只需要针对每个示例 AUT 执行此操作一次,在测试自己的 AUT 时无需执行。

在基于 Unix 的系统上,可执行文件可能是一个脚本,如 addressbook.tcl,而在 Windows 上,可能需要 ActiveState 安装的 tclsh,在这种情况下,参数 应为 addressbook.tcl工作目录 应为该脚本的绝对路径。

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 文档主要在提到 GUI 对象(例如按钮、菜单、菜单项、标签和表格控件)时使用术语 widget。Windows 用户可能更熟悉术语 controlcontainer,但在这里我们使用术语 widget 指两者。同样,macOS 用户可能对术语 view 颇为熟悉。

创建测试套件

测试套件是一组测试案例(测试)。使用测试套件很方便,因为它使得轻松在一系列相关的测试之间共享脚本和测试数据变得容易。

在这里,以及在整个教程中,我们将首先描述如何使用 squishide 进行操作,接着会为命令行用户提供信息。

squishide 创建测试套件

通过单击或双击 squishide 图标、从任务栏菜单启动 squishide 或在命令行上执行 squishide 来启动 squishide, whichever you prefer and find suitable for the platform you are using. 一旦 Squish 启动,您可能会看到一个 欢迎页面。点击右上角的 工作台 按钮来关闭它。然后,squishide 将看起来与截图相似,但根据您使用的窗口系统、颜色、字体和主题可能略有不同。

"The Squish IDE with no Test Suites"

单击文件 > 新建测试套件,将弹出新测试套件向导,如下所示。

"Name & Directory page"

为您的测试套件输入名称,并选择您想要存储测试套件的文件夹。在截图中,我们将测试套件命名为suite_tcl,并将其放置在addressbook文件夹中。(对于您自己的测试,您可能需要使用更有意义的名称,例如"suite_addressbook";我们选择"suite_tcl",因为教程包含了Squish支持的所有脚本语言的测试套件。)当然,您可以自由选择您喜欢的名称和文件夹。一旦详细信息填写完成,请单击下一步以进入工具箱(或脚本语言)页面。

"Toolkit page"

如果出现此向导页面,请点击您AUT使用的工具箱。对于本例,我们必须点击Tk,因为我们正在测试Tk应用程序。然后单击下一步以转到脚本语言页面。

"Scripting Language page"

选择您想要的任意脚本语言——唯一的限制是您在测试套件中只能使用一种脚本语言。当然,如果您是Tcl/Tk程序员,Tcl将是您的选择。选择脚本语言后,再次单击下一步以进入向导的最后一页。

"AUT page"

如果您正在为Squish已知的AUT创建新的测试套件,只需单击组合框,下拉AUT列表并选择您想要的。如果组合框为空或您的AUT未列出,请单击组合框右侧的浏览按钮——这将弹出一个文件打开对话框,您可以从其中选择AUT。一旦选择了AUT,请单击完成。Squish将为测试套件创建一个同名的子文件夹,并在该文件夹中创建一个名为suite.conf的文件,其中包含测试套件的配置详细信息。Squish还将AUT注册到squishserver。然后,向导将关闭,squishide将类似于下面的截图。

"The suite_tcl test suite"

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

从命令行创建测试套件

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

  1. 创建一个新目录以存放测试套件——目录的名称应以suite开头。对于本教程,我们创建了SQUISHDIR/examples/tk/addressbook/suite_tcl目录用于Tcl测试。(我们还有其他类似子目录,但这只是为了示例,因为通常我们只使用一种语言进行所有测试。)
  2. 在测试套件子目录中创建一个名为suite.conf的纯文本文件(ASCII或UTF-8编码)。这是测试套件的配置文件,它至少必须识别AUT、用于测试的脚本语言和AUT使用的包装器(即GUI工具箱或库)。文件的格式为key = value,每行一个键值对。例如
    AUT      = addressbook.tcl
    LANGUAGE = Tcl
    WRAPPERS = Tk

    在Windows上,它可能看起来像这样

    AUT      = tclsh addressbook.tcl
    CWD      = SQUISHDIR/examples/tk/addressbook
    LANGUAGE = Tcl
    WRAPPERS = Tk

    AUT可以是Tcl脚本(在*nix系统上),也可以是tclsh可执行文件,或一个批处理文件/shell脚本,该脚本以前台方式启动Tk AUT。LANGUAGE可以设置为您首选的脚本语言。WRAPPERS应设置为Tk

    在Windows上,由于Tcl脚本可能不可执行,我们将AUT设置为tclsh,并提供Tcl脚本的名称作为参数。我们还必须设置CWD为AUT脚本addressbook.tcl的位置。

  3. 将AUT注册到squishserver。

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

    可以通过在命令行中运行 squishserver 并使用 --config 选项和 addAUT 命令来执行此操作。例如,假设我们位于 SQUISHDIR/bin 目录中,在 Linux 系统上

    ./squishserver --config addAUT addressbook.tcl $SQUISHDIR/examples/tk/addressbook

    Windows 用户可以添加一个将 AUT 启动的 .bat 文件,或将 tclsh.exe 作为 AUT 如下所示

    squishserver --config addAUT tclsh.exe C:\TCL\bin

    必须给 addAUT 命令提供 AUT 可执行文件的名字,以及 separately——AUT 的路径。有关应用程序路径的更多信息,请参阅 AUTs and Settings,有关 squishserver 命令行选项的更多信息, please see squishserver

录制测试和验证点

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()你好,世界日志输出到测试结果中。要手动创建一个测试,就像我们将在教程中稍后做的那样,我们必须创建一个main函数,并且在顶部应该导入相同的导入。对于Squish来说,main的名称是特殊的。测试可以包含你喜欢的任意数量的函数和其他代码,这取决于脚本语言的支持。但是,当测试执行时(也就是运行时),Squish总是执行main函数。你可以根据如何创建和使用共享数据以及共享脚本所述,在测试脚本之间分享常用的代码。

还有两个函数名称对于Squish来说也是特殊的:cleanupinit。更多信息,请见测试者创建的特殊函数

一旦创建了新的测试用例,我们就可以自由地手动编写测试代码,或者记录一个测试。点击测试用例的记录按钮(如何编辑和调试测试脚本中的指示,记录片段并将其插入到现有的测试用例中。

从命令行创建测试

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

  1. 在测试套件目录内创建一个新的子目录。例如,在SQUISHDIR/examples/tk/addressbook/suite_tcl目录内,创建tst_general目录。
  2. 在测试用例目录内创建一个名为test.tcl的空文件(如果你使用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”,然后点击确认以确认更改。
  4. 现在点击第一行,然后点击编辑 > 删除。第一行应该消失,因此“Jane Doe”条目现在是第一个。
  5. 点击第一行,然后点击 编辑 > 编辑 以弹出编辑对话框(应显示“Jane Doe”条目的详细信息)。
  6. 现在点击Squish 控制栏窗口 中的 验证 工具栏按钮(从左数第二个按钮)并选择 属性

    {}

    这将使 squishide 出现。

  7. 在应用程序对象视图下展开 addressbook.tcl Tk_Window 对象,然后展开 dialog 对象。点击“forename”对象以使其属性显示在 属性视图 中,然后检查 getvalue 属性的复选框。现在点击“surname”对象并检查其 getvalue 属性。
  8. 最后,点击 保存并插入验证 按钮(位于 验证点创建器视图 的底部)以将第一行的forename和surname验证插入到录制测试脚本中。(见下面的截图)一旦插入验证点,squishide 的窗口将再次隐藏,控制栏窗口和 AUT 将再次显示。
  9. 现在已经完成了测试计划,因此在使用 AUT 时,点击 取消 来关闭编辑对话框,然后点击 文件 > 退出,然后在消息框中点击 ,因为我们不想保存任何更改。

"Two verification points about to be inserted"

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

"The recorded tst_general test"

如果记录的测试未出现,请点击(或根据您的平台和设置双击)tst_general 测试案例;这将使 Squish 在一个编辑器窗口中显示测试的 test.tcl 文件,如图所示。

现在我们已经记录了测试,我们可以回放它,即运行它。现在,我们将检查我们放入的两项验证,如图所示。

在测试录制过程中插入验证点非常方便。这里我们一次插入两个,但我们可以在测试录制过程中根据需要插入尽可能多的验证点。然而,有时我们可能忘记了插入一个,或者以后可能想插入一个新的。我们可以在记录的测试脚本中插入附加的验证,如下一节 插入附加验证点 所述。

在继续之前,我们将看看如何从命令行录制测试。然后我们将了解如何运行测试,并查看 Squish 录制测试时生成的某些代码,并讨论其一些特性。

从命令行录制测试

在记录测试时,squishserver 必须始终运行。这由 squishide 自动处理,但命令行用户必须按照 squishserver 中的说明手动启动 squishserver。

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

squishrunner --testsuite suite_tcl --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_tcl --testcase tst_general --local

检查生成的代码

如果您查看截图中的代码(或下面显示的代码片段),您会发现它包含大量的 Object waitForObject(objectOrName) 调用,作为其他各种调用的参数,例如 activateItem(objectName, itemText)clickItem(objectName, itemIdentifier, x, y, modifierState, button)clickButton(objectName)type(objectName, text),以及许多 Object waitForObjectItem(objectOrName, itemOrIndex) 调用。Object waitForObject(objectOrName)Object waitForObjectItem(objectOrName, itemOrIndex) 函数会等待 GUI 对象准备好交互(即变为可见和启用),然后之后会跟随一些与对象交互的功能。典型的交互操作包括激活(弹出)菜单、点击菜单选项或按钮或输入一些文本。

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

对象通过 Squish 生成的名称来标识。有关完整详细信息,请参阅 如何识别和访问对象

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

注意: 尽管截图仅展示了 Python 测试套件的运行情况,但在此处和对整个教程中引用的代码片段,我们展示了 Squish 支持的所有脚本语言的代码。当然,在实践中你通常只会使用其中之一,所以请自由地只查看你感兴趣的代码片段,而忽略其他。

    waitForObjectItem $names::addressbook_tcl_menuBar_2 "Edit"
    invoke activateItem $names::addressbook_tcl_menuBar_2 "Edit"
    waitForObjectItem $names::addressbook_tcl_menuBar_edit_2 "Add..."
    invoke activateItem $names::addressbook_tcl_menuBar_edit_2 "Add..."
    invoke type [waitForObject $names::addressbook_tcl_dialog_forename_2] "<Shift_L>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_forename_2] "Jane"
    invoke type [waitForObject $names::addressbook_tcl_dialog_forename_2] "<Tab>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_surname_2] "<Shift_L>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_surname_2] "Doe"
    invoke type [waitForObject $names::addressbook_tcl_dialog_surname_2] "<Tab>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "555"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "<space>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "123"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "<space>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "4567"
    invoke type [waitForObject $names::addressbook_tcl_dialog_phone_2] "<Tab>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "jane"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "<period>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "doe@"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "nowhere"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "<period>"
    invoke type [waitForObject $names::addressbook_tcl_dialog_email_2] "com"
    invoke clickButton [waitForObject $names::addressbook_tcl_dialog_buttonarea_ok_2]
    waitForObjectItem(names.addressbook_tcl_menuBar, "Edit")
    activateItem(names.addressbook_tcl_menuBar, "Edit")
    waitForObjectItem(names.addressbook_tcl_menuBar_edit, "Add...")
    activateItem(names.addressbook_tcl_menuBar_edit, "Add...")
    type(waitForObject(names.addressbook_tcl_dialog_forename), "<Shift_L>")
    type(waitForObject(names.addressbook_tcl_dialog_forename), "J", 1)
    type(waitForObject(names.addressbook_tcl_dialog_forename), "ane")
    type(waitForObject(names.addressbook_tcl_dialog_forename), "<Tab>")
    type(waitForObject(names.addressbook_tcl_dialog_surname), "<Shift_L>")
    type(waitForObject(names.addressbook_tcl_dialog_surname), "D", 1)
    type(waitForObject(names.addressbook_tcl_dialog_surname), "oe")
    type(waitForObject(names.addressbook_tcl_dialog_surname), "<Tab>")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "123")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "<space>")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "555")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "<space>")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "1212")
    type(waitForObject(names.addressbook_tcl_dialog_phone), "<Tab>")
    type(waitForObject(names.addressbook_tcl_dialog_email), "jane")
    type(waitForObject(names.addressbook_tcl_dialog_email), "<period>")
    type(waitForObject(names.addressbook_tcl_dialog_email), "dow")
    type(waitForObject(names.addressbook_tcl_dialog_email), "<Shift_L>")
    type(waitForObject(names.addressbook_tcl_dialog_email), "<at>", 1)
    type(waitForObject(names.addressbook_tcl_dialog_email), "nowhere")
    type(waitForObject(names.addressbook_tcl_dialog_email), "<period>")
    type(waitForObject(names.addressbook_tcl_dialog_email), "com")
    type(waitForObject(names.addressbook_tcl_dialog_email), "<Return>")
    waitForObjectItem(names.addressbookTclMenuBar, "Edit");
    activateItem(names.addressbookTclMenuBar, "Edit");
    waitForObjectItem(names.addressbookTclMenuBarEdit, "Add...");
    activateItem(names.addressbookTclMenuBarEdit, "Add...");
    type(waitForObject(names.addressbookTclDialogForename), "<Shift_L>");
    type(waitForObject(names.addressbookTclDialogForename), "Jane");
    type(waitForObject(names.addressbookTclDialogForename), "<Tab>");
    type(waitForObject(names.addressbookTclDialogSurname), "<Shift_L>");
    type(waitForObject(names.addressbookTclDialogSurname), "Doe");
    type(waitForObject(names.addressbookTclDialogSurname), "<Tab>");
    type(waitForObject(names.addressbookTclDialogPhone), "555");
    type(waitForObject(names.addressbookTclDialogPhone), "<space>");
    type(waitForObject(names.addressbookTclDialogPhone), "123");
    type(waitForObject(names.addressbookTclDialogPhone), "<space>");
    type(waitForObject(names.addressbookTclDialogPhone), "4567");
    type(waitForObject(names.addressbookTclDialogPhone), "<Tab>");
    type(waitForObject(names.addressbookTclDialogEmail), "jane");
    type(waitForObject(names.addressbookTclDialogEmail), "<period>");
    type(waitForObject(names.addressbookTclDialogEmail), "doe@");
    type(waitForObject(names.addressbookTclDialogEmail), "nowhere");
    type(waitForObject(names.addressbookTclDialogEmail), "<period>");
    type(waitForObject(names.addressbookTclDialogEmail), "com");
    clickButton(waitForObject(names.addressbookTclDialogButtonareaOk));
    waitForObjectItem($Names::addressbook_tcl_menubar, "Edit");
    activateItem($Names::addressbook_tcl_menubar, "Edit");
    waitForObjectItem($Names::addressbook_tcl_menubar_edit, "Add...");
    activateItem($Names::addressbook_tcl_menubar_edit, "Add...");
    type(waitForObject($Names::addressbook_tcl_dialog_forename), "<Shift_L>");
    type(waitForObject($Names::addressbook_tcl_dialog_forename), "Jane");
    type(waitForObject($Names::addressbook_tcl_dialog_forename), "<Tab>");
    type(waitForObject($Names::addressbook_tcl_dialog_surname), "<Shift_L>");
    type(waitForObject($Names::addressbook_tcl_dialog_surname), "Doe");
    type(waitForObject($Names::addressbook_tcl_dialog_surname), "<Tab>");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "555");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "<space>");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "123");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "<space>");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "4567");
    type(waitForObject($Names::addressbook_tcl_dialog_phone), "<Tab>");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "jane");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "<period>");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "doe\@");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "nowhere");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "<period>");
    type(waitForObject($Names::addressbook_tcl_dialog_email), "com");
    clickButton(waitForObject($Names::addressbook_tcl_dialog_buttonarea_ok));
    waitForObjectItem(Names::Addressbook_tcl_menuBar, "Edit")
    activateItem(Names::Addressbook_tcl_menuBar, "Edit")
    waitForObjectItem(Names::Addressbook_tcl_menuBar_edit, "Add...")
    activateItem(Names::Addressbook_tcl_menuBar_edit, "Add...")
    type(waitForObject(Names::Addressbook_tcl_dialog_forename), "<Shift_L>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_forename), "J", 1)
    type(waitForObject(Names::Addressbook_tcl_dialog_forename), "ane", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_forename), "<Tab>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_surname), "<Shift_L>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_surname), "D", 1)
    type(waitForObject(Names::Addressbook_tcl_dialog_surname), "oe", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_surname), "<Tab>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "555", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "<space>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "123", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "<space>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "4567", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_phone), "<Tab>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "jane", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "<period>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "doe", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "<Shift_L>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "<Shift_L>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "<at>", 1)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "nowhere", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "<period>", 0)
    type(waitForObject(Names::Addressbook_tcl_dialog_email), "com", 0)
    clickButton(waitForObject(Names::Addressbook_tcl_dialog_buttonarea_ok))

在录制上述测试时,测试人员使用键盘在文本字段之间切换标签,并用鼠标点击确定而不是按键盘。如果测试人员以任何其他方式单击按钮(例如,通过切换到“确定”按钮并按空格键),结果应该是相同的,但 Squish 将记录不同的操作。

对象名称

有些对象名称是以冒号开头的字符串。这些是符号名。Squish 支持几种命名方案,都可以在脚本中使用——并且可以混合使用。

使用符号名的优点是,如果应用方式发生变化导致生成不同的名称,我们可以简单地更新 Squish 的对象映射(将符号名称与实际名称关联),从而避免需要更改我们的测试脚本。有关对象映射的更多信息,请参阅对象映射对象映射视图

与 Squish 的其他版本不同,Squish/Tk 中使用的实际名称是分层的,而不是多属性名称。这意味着对象名称代表存储在内存中 GUI 的对象树中类名称的路径。符号名是基于这个层次结构。

现在我们已经了解了如何录制和回放测试,也看到了 Squish 生成的代码,那么让我们更进一步,确保在测试执行的特定点上某些条件保持不变。

插入额外的验证点

在上一节中,我们看到了如何在录制测试脚本时插入验证点是多么简单。验证点也可以插入到现有的测试脚本中,通过设置断点和使用squishide,或者简单编辑测试脚本并插入对 Squish 的测试函数的调用,如布尔 test.compare(value1, value2)布尔 test.verify(condition)

Squish 支持四种类型的验证点:验证对象属性具有特定值——称为“对象属性验证”;验证整个表具有我们预期的内容——称为“表验证”;验证两个图像是否匹配——称为“截图验证”;以及包含多个对象的属性和截图的混合验证类型,称为“视觉验证”。最常用的类型是对象属性验证,这也是我们在教程中将涵盖的内容。有关更多信息,请参阅如何创建和使用验证点)。

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

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

在实际请求Squish插入验证点之前,最好确保我们有一个想要验证的列表和时机。我们可以在tst_general测试案例中添加许多潜在的验证,但鉴于我们在这里只是想展示如何进行,我们将只做两步——我们将验证"Jane Doe"条目的电子邮件地址和电话号码是否与我们输入的相匹配,并将验证点立即放在我们录制过程中插入的验证之后。

要使用squishide插入验证点,我们首先在脚本中(无论记录的还是手工写的——对Squish来说无关紧要)在想要验证的点设置一个断点。

"The tst_general test case with a breakpoint"

就像上面的截图所示,我们在第71行设置了断点。这可以通过右键单击行号然后在上下文菜单中点击切换断点菜单项来完成。我们选择这一行,因为它位于脚本行后面,第一地址被删除的地方,以及我们调用了编辑对话框的地方。因此,在这个点(在调用文件菜单关闭应用程序之前),第一个地址(以及编辑对话框中显示的地址)应该是"Jane Doe"的。截图显示了在录制过程中使用squishide输入的验证。我们的额外验证将跟随它们。(请注意,如果你以不同的方式录制测试,例如使用键盘快捷键而不是点击菜单项,你的行号可能会有所不同。)

设置断点后,我们像往常一样通过点击运行测试 )或通过点击运行 > 运行测试案例菜单选项来运行测试。与正常测试运行不同,当到达断点时(即在第71行,或你设置的任何行)测试将停止,Squish的主窗口将再次出现(这可能掩盖了应用程序)。此时,squishide将自动切换到测试调试视角

视角和视图

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

squishide预装以下视角:

您可以通过更改这些视角来显示附加视图或隐藏不想要的视图,或者创建包含您想要视图的视角。因此,如果您的窗口发生了巨大变化,这只意味着视角已更改。使用窗口菜单切换到您想要的视角。然而,由于Squish会自动将视角更改以反映当前情况,所以您通常不需要手动更改视角。

插入验证点

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

要插入一个验证点,我们可以展开应用程序对象视图中的项目,直到找到我们想要验证的对象。在这个例子中,我们想要验证表格的第一行的文本,所以我们展开addressbook.tcl Tk_Window对象,然后是dialog对象以找到我们感兴趣的项目:在这个例子中是phoneemail项目。一旦我们单击一个对象,它的属性就会在如图所示的属性视图中显示。

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

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

在此,我们可以看到前缀项的getvalue属性值为"Jane";我们在记录期间已经为此插入了一个验证。向下滚动,以便您可以查看电话项。要确保每次测试运行时都进行验证,请单击应用程序对象视图中的电话项以显示其属性,然后单击getvalue属性以检查其复选框。当我们检查它时,如图所示的验证点创建器视图就会出现。

"Choosing a property value to verify"

此时,验证点尚未添加到测试脚本中。我们可以很容易地通过单击保存并插入验证按钮来添加它。但在做之前,我们将添加另一个要验证的项目。

向下滚动并单击应用程序对象视图中的电子邮件项;然后单击其getvalue属性。现在,两个验证都将如图所示的出现在验证点创建器视图中。

"Choosing several property values to verify"

我们现在已经声明,我们期望这些属性具有显示的值,即电子邮件地址为"[email protected]"和电话号码为"555 123 4567"。我们必须单击插入按钮才能真正插入验证点,所以请现在做。

现在我们不需要继续运行测试,因此我们可以通过单击停止工具栏按钮在此处停止测试运行,或者我们可以继续(通过单击继续按钮)。

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

"Newly inserted verification points"

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

另一种插入验证点的方法是将它们以代码形式插入。理论上,我们可以在现有脚本的任何地方添加自己的对 Squish 测试函数的调用,例如 Booleans test.compare(value1, value2)Booleans test.verify(condition)。但在实际应用中,最好确保 Squish 已知我们想要验证的对象,这样它才能在测试运行时找到它们。这涉及与使用 squishide 非常相似的程序。首先,我们在打算增加验证的地方设置一个断点。然后运行测试脚本,直到它停止。接下来在 应用程序对象视图 中浏览,直到找到我们想要验证的对象。此时,右键单击感兴趣的对象,然后在上下文菜单中单击 添加到对象映射 菜单选项是明智的。这将确保 Squish 可以访问该对象。然后再次右键单击并单击 复制到剪贴板(符号名) 上下文菜单选项——这将给我们 Squish 将使用来识别该对象的名称。现在我们可以编辑测试脚本以添加我们的验证并完成或停止执行。(一旦不再需要,别忘了禁用断点。)

虽然我们可以将测试脚本代码编写成与自动生成的代码完全相同的风格,但通常以略为不同的风格做事情会更清晰、更容易,我们将在接下来的段落中解释。

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

"Manually entered verification points"

当手工编写脚本时,我们使用 Squish 的 test 模块的函数,在测试脚本执行过程中某些点上验证条件。就像截图(和下面的代码片段)所示,我们首先确保我们感兴趣的对象可用。对于手工编写的测试脚本,使用 Object waitForObject(objectOrName) 函数是标准做法。此函数等待对象可用(即可见并可选)。(否则它会超时并引发可捕获的异常。)一旦我们知道对象可用,我们使用 String tcleval(code) 函数与对象交互——在这种情况下,检索行数(该行报告为“大小”)。我们使用 Booleans test.compare(value1, value2) 函数将实际行数与预期数进行比较。请注意,Squish 函数通常需要一个带前缀的自动运行时名称,但 String tcleval(code) 函数不需要该前缀。

以下是我们在首次验证中手动输入的所有Squish支持脚本语言的代码。当然,您只需查看自己测试将要使用的语言的代码。在Tcl中,通常更方便使用布尔测试.compare(value1, value2)函数,但我们当然可以使用Squish的任何test函数,例如我们为其他脚本语言使用的布尔测试.verify(condition)函数。

    waitForObject $names::addressbook_tcl_view_tree_2
    set rows [invoke tcleval ".view.tree size"]
    test compare $rows "125"
    waitForObject(names.addressbook_tcl_view_tree_body)
    rows = tcleval(".view.tree size")
    test.compare(cast(rows, int), expected_rows)
    waitForObject(names.addressbookTclViewTree);
    var rows = tcleval(".view.tree size");
    test.verify(parseInt(rows) == 125);
    waitForObject($Names::addressbook_tcl_view_tree);
    my $rows = tcleval(".view.tree size");
    test::verify($rows eq 125);
    waitForObject(Names::Addressbook_tcl_view_tree)
    rows = tcleval(".view.tree size")
    Test.verify(rows == "125")

编码模式非常简单:我们确保我们感兴趣的对象可用,使用字符串tcleval(code)函数访问其属性,然后使用Squish的验证函数验证结果。

有关手动编写的测试示例的更多信息,请参阅通过手工创建测试如何创建测试脚本如何测试应用程序 - 特定信息

为了完整覆盖验证点,请参阅如何创建和使用验证点

测试结果

每次测试运行完成后,包括验证点在内的测试结果都会在`squishide`底部的测试结果视图中显示。

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

Squish的测试结果界面非常灵活。通过实现自定义报告生成器,可以以多种方式处理测试结果,例如将它们存储在数据库中,或以HTML文件输出。默认报告生成器在从命令行运行Squish时将结果打印到stdout,或在使用`squishide`时打印到XML和测试结果视图。您可以通过在测试结果上右键单击并选择>`导出`按钮或菜单操作从`squishide`中导出XML测试结果。

如果您使用squishrunner在命令行上运行测试,您也可以以不同的格式导出结果并将它们保存到文件中。有关可用报告生成器的列表,请参阅squishrunner –reportgen: 生成报告。有关更多信息,请参阅处理测试结果如何使用测试语句部分。您还可以将测试结果直接记录到数据库中,如如何从Squish测试脚本访问数据库中所述。

通过手工创建测试

既然我们已经看到如何录制测试并通过插入验证点进行修改,我们现在可以了解如何手动创建测试。最简单的方法是修改和重构已录制的测试,尽管从头开始创建手动测试也是完全可以的。

编写手动测试最具挑战性的部分可能是使用合适的对象名称,但在实践中,这很少成为问题。我们可以复制Squish在记录之前测试时已经添加到对象映射表中的符号名称,或者直接从记录的测试中复制对象名称。如果我们还没有录制任何测试,从头开始,可以使用Spy。我们通过点击工具栏上的启动AUT按钮来实现。这将启动AUT并切换到Spy视角。然后,我们可以与AUT交互,直到感兴趣的我们看到。然后,在squishide中,我们可以导航到应用程序对象视图中的对象,并使用上下文菜单既添加到对象映射(这样Squish会记住它),又复制真实名称到剪贴板(这样我们就可以将其粘贴到我们的测试脚本中)。完成之后,我们可以点击工具栏上的退出AUT按钮来终止AUT并返回Squish到测试管理视角

要查看特定符号名称的对象映射条目,可以在编辑视图中将其突出显示,右键单击并选择打开符号名称。我们还可以通过单击对象映射 () 按钮查看对象映射(另见对象映射视图)。Squish与之交互的每个应用程序对象都列在这里,无论是顶级对象还是子对象(视图是树状视图)。我们可以通过右键单击对象映射条目并单击复制对象名称获取用于它的对象名称,或者如果需要一个字符串,则单击复制真实名称。这在我们想要修改现有的测试脚本或从头开始创建测试脚本时很有用,如下文所述教程中我们将看到。

"Squish Object Map"

修改和重构记录的测试

假设我们想通过添加三个新的姓名和地址来测试AUT的添加功能。我们可以记录这样的测试,但可能手动编码会更容易。测试脚本需要执行的步骤是:首先点击文件 > 新建 创建新的通讯录,然后针对每个新名称和地址

  • 点击编辑 > 添加
  • 填写详细资料。
  • 点击确定

最后,点击文件 > 退出 并不保存。

我们还要在开始时验证没有数据行,并在结束时有三行。我们将边修改边重构,以便使我们的代码尽可能整洁和模块化。

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

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

我们首先需要一种方法来启动AUT并调用菜单选项。以下是从已记录tst_general脚本中前几行的示例

proc main {} {
    startApplication "addressbook.tcl"

    # Open file with addresses
    snooze 1
    invokeMenuItem "File" "Open..."
def main():
    startApplication("addressbook.tcl")

    # Open file with addresses
    asyncMouseClick(waitForObject(names.addressbook_tcl_toolBar_fileOpen), 5, 5, 0, 1)
function main()
{
    startApplication("addressbook.tcl");

    // Open file with addresses
    snooze(1)
    invokeMenuItem("File", "Open...")
sub main
{
    startApplication("addressbook.tcl");
    waitForObjectItem($Names::addressbook_tcl_menubar, "File");
    activateItem($Names::addressbook_tcl_menubar, "File");
    snooze(1);
    waitForObjectItem($Names::addressbook_tcl_menubar_file, "Open...");
    activateItem($Names::addressbook_tcl_menubar_file, "Open...");
def main
    startApplication("addressbook.tcl")

    # Open file with addresses
    asyncMouseClick(waitForObject(Names::Addressbook_tcl_toolBar_fileOpen), 5, 5, 0, 1)

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

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

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

Error Script Error Apr 9, 2010
Detail LookupError: Item 'New...' in object ':addressbook.tcl' not found or ready.
Called from: C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl: 18
Location C:\squish\examples\tk\addressbook\suite_py\tst_adding\test.tcl:3

这通常意味着Squish在对象映射中没有给定名称的对象或属性值。从这里,我们可以 选择新的对象调试抛出错误,或者在选择了新的对象之后,重试。除了Spy的 对象选择器 () 外,我们还可以使用 应用程序对象视图 定位我们感兴趣的物体,并通过上下文菜单操作 添加到对象映射。一般来说,记录一个与所有相关AUT对象交互的虚拟测试案例是一个更有效的方式来最初填充对象映射。

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

我们现在几乎可以编写自己的测试脚本了。最简单的方法是从录制一个虚拟测试开始。所以 点击 文件 > 新建测试案例 并将测试案例的名称设置为 tst_dummy。然后点击虚拟测试案例的 记录 ()。一旦AUT开始,点击 文件 > 新建,然后点击表格,然后点击 编辑 > 添加 添加一个条目,然后按 Return 或点击 确定。最后,点击 文件 > 退出 以完成,并选择不保存更改。

重新播放这个测试只是为了确认一切正常。这样做的主要目的就是确保Squish将必要的名称添加到对象映射中,因为这样做可能比用Spy为每个感兴趣的物体都做得更快。重新播放虚拟测试后,如果你想,可以将其删除。

proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 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]
    }
    verifyRows [llength $data]
    closeWithoutSaving
}
def main():
    startApplication("tclsh addressbook.tcl")

    # Create new file
    clickButton(waitForObject(names.addressbook_tcl_toolBar_fileNew))

    # Verify row count
    verifyRows(0)

    # Add data row by row
    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)

    # Verify row count
    verifyRows(len(data))

    closeWithoutSaving()
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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]);
    verifyRows(data.length);
    closeWithoutSaving();
}
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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});
    }
    verifyRows(scalar(@data));
    closeWithoutSaving;
}
def main
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(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
    verifyRows(data.length)
    closeWithoutSaving
end

我们从一个使用 ApplicationContext startApplication(autName) 函数调用来启动应用程序开始。我们传递的字符串是Squish注册的名称(通常是可执行文件的名称)。

我们专门为这次测试创建了一个名为 invokeMenuItem 的函数。它接受一个菜单名称和一个菜单选项名称,并调用菜单选项。使用 invokeMenuItem 函数进行 文件 > 新建 操作后,我们验证表格的行数是 0。当只是想验证一个条件是否为真而不是比较两个不同的值时,布尔测试.verify(condition) 函数很有用。(对于 Tcl,我们通常使用 布尔测试.compare(value1, value2) 函数,而不是 布尔测试.verify(condition) 函数,因为它在使用上稍简单一些。)

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

我们现在将逐一审查这四个辅助函数(以及一个第五个辅助函数),以便涵盖 tst_adding 测试用例中的所有代码,从 invokeMenuItem 开始。

proc invokeMenuItem {menu item} {
    waitForObjectItem $names::addressbook_tcl_menuBar_2 $menu
    invoke activateItem $names::addressbook_tcl_menuBar_2 $menu
    set menuName [string tolower $menu]
    waitForObjectItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
    invoke activateItem ":addressbook\\.tcl.#menuBar.#$menuName" $item
}
def invokeMenuItem(menu, item):
    waitForObjectItem(names.addressbook_tcl_menuBar, menu)
    activateItem(names.addressbook_tcl_menuBar, menu)
    waitForObjectItem(names.addressbook_tcl_menuBar + ".#%s" % menu.lower(), item)
    activateItem(names.addressbook_tcl_menuBar + ".#%s" % menu.lower(), item)
function invokeMenuItem(menu, item)
{
    waitForObjectItem(names.addressbookTclMenuBar, menu);
    activateItem(names.addressbookTclMenuBar, menu);
    var menuText = menu.toLowerCase();
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
    activateItem(":addressbook\\.tcl.#menuBar.#" + menuText, item);
}
sub invokeMenuItem
{
    my ($menu, $item) = @_;
    waitForObjectItem($Names::addressbook_tcl_menubar, $menu);
    activateItem($Names::addressbook_tcl_menubar, $menu);
    my $menuText = lc $menu;
    waitForObjectItem($Names::addressbook_tcl_menubar . ".#$menuText", $item);
    activateItem($Names::addressbook_tcl_menubar . ".#$menuText", $item);
}
def invokeMenuItem(menu, item)
    waitForObjectItem(Names::Addressbook_tcl_menuBar, menu)
    activateItem(Names::Addressbook_tcl_menuBar, menu)
    waitForObjectItem(":addressbook\\.tcl.#menuBar.#%s" % menu.downcase, item)
    activateItem(":addressbook\\.tcl.#menuBar.#%s" % menu.downcase, item)
end

符号名称基于对象在其 GUI 中的层次结构。在 Squish/Tk 中,真实名称是表示该层次结构的字符串。在这里,我们使用了 Squish 生成的字符串名称,但对于菜单和项目,我们对标签文本进行了参数化。

一旦我们确定了要与之交互的对象,我们就使用 Object waitForObjectItem(objectOrName, itemOrIndex) 函数获取一个引用,然后在这个例子中我们对它应用 activateItem(objectName, itemText) 函数。函数 Object waitForObjectItem(objectOrName, itemOrIndex) 会暂停 Squish,直到指定的对象及其项可见和启用。因此,在这里,我们等待菜单栏及其一个菜单栏项,然后等待一个菜单栏项及其一个菜单项。然后我们在每次等待结束后使用 activateItem(objectName, itemText) 函数激活对象及其项。

proc verifyRows {expected_rows} {
    waitForObject $names::addressbook_tcl_view_tree_2
    set rows [invoke tcleval ".view.tree size"]
    test compare $rows "$expected_rows"
}
def verifyRows(expected_rows):
    waitForObject(names.addressbook_tcl_view_tree_body)
    rows = tcleval(".view.tree size")
    test.compare(cast(rows, int), expected_rows)
function verifyRows(expected_rows)
{
    waitForObject(names.addressbookTclViewTree);
    var rows = tcleval(".view.tree size");
    test.verify(parseInt(rows) == expected_rows);
}
sub verifyRows
{
    my $expected_rows = shift;
    waitForObject($Names::addressbook_tcl_view_tree);
    my $rows = tcleval(".view.tree size");
    test::verify($rows eq $expected_rows);
}
def verifyRows(expected_rows)
    waitForObject(Names::Addressbook_tcl_view_tree)
    rows = tcleval(".view.tree size")
    Test.compare(String(rows), String(expected_rows))
end

而不是在两个不同的地方复制验证行数的三个线条,我们将功能打包在一个小的函数中。对于 Python 版本,我们使用了 Object cast(object, type) 函数,因为 Squish 有自己的 int 对象;另请参阅 Squish 的 Python 模块

proc addNameAndAddress {oneNameAndAddress} {
    invokeMenuItem "Edit" "Add..."
    set fieldNames [list "forename" "surname" "phone" "email"]
    for {set field 0} {$field < [llength $fieldNames]} {incr field} {
        set fieldName [lindex $fieldNames $field]
        set text [lindex $oneNameAndAddress $field]
        invoke type [waitForObject ":addressbook\\.tcl.dialog.$fieldName"] $text
    }
    invoke clickButton [waitForObject $names::addressbook_tcl_dialog_buttonarea_ok_2]
}
def addNameAndAddress(oneNameAndAddress):
    invokeMenuItem("Edit", "Add...")
    for fieldName, text in zip(("forename", "surname", "phone", "email"), oneNameAndAddress):
        type(waitForObject(names.addressbook_tcl_dialog + ".%s" % fieldName), text)
    clickButton(waitForObject(names.addressbook_tcl_dialog_buttonarea_ok))
function addNameAndAddress(oneNameAndAddress)
{
    invokeMenuItem("Edit", "Add...");
    var fieldNames = new Array("forename", "surname", "phone", "email");
    for (var i = 0; i < oneNameAndAddress.length; ++i) {
        var fieldName = fieldNames[i];
        var text = oneNameAndAddress[i];
        type(waitForObject(":addressbook\\.tcl.dialog." + fieldName), text);
    }
    clickButton(waitForObject(":addressbook\\.tcl.dialog.buttonarea.ok"));
}
sub addNameAndAddress
{
    my (@oneNameAndAddress) = @_;
    invokeMenuItem("Edit", "Add...");
    my @fieldNames = ("forename", "surname", "phone", "email");
    my $fieldName = "";
    for (my $i = 0; $i < scalar(@fieldNames); $i++) {
        $fieldName = $fieldNames[$i];
        my $text = $oneNameAndAddress[$i];
        type(waitForObject($Names::addressbook_tcl_dialog . ".$fieldName"), $text);
    }
    clickButton(waitForObject($Names::addressbook_tcl_dialog_buttonarea_ok));
}
def addNameAndAddress(oneNameAndAddress)
    invokeMenuItem("Edit", "Add...")
    ["forename", "surname", "email", "phone"].each_with_index do
        |fieldName, index|
        text = oneNameAndAddress[index]
        type(waitForObject(":addressbook\\.tcl.dialog.#{fieldName}"), text)
    end
    clickButton(waitForObject(Names::Addressbook_tcl_dialog_buttonarea_ok))
end

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

proc closeWithoutSaving {} {
    invokeMenuItem "File" "Quit"
    snooze 1
    invoke nativeType "<Tab>"
    invoke nativeType "<Return>"
}
def closeWithoutSaving():
    invokeMenuItem("File", "Quit")
    snooze(1)
    nativeType("<Tab>")
    nativeType("<Return>")
function closeWithoutSaving()
{
    invokeMenuItem("File", "Quit");
    snooze(1);
    nativeType("<Tab>");
    nativeType("<Return>")
}
sub closeWithoutSaving
{
    invokeMenuItem("File", "Quit");
    snooze(1);
    nativeType("<Tab>");
    nativeType("<Enter>");
}
def closeWithoutSaving
    invokeMenuItem("File", "Quit")
    snooze(1)
    nativeType("<Tab>")
    nativeType("<Return>")
end

这里我们使用 invokeMenuItem 来做 文件 > 退出,然后点击 保存未保存的更改? 对话框的 。最后一行是从记录的测试中复制过来的。

整个测试大约有 75 行代码——如果我们将通用函数(如 invokeMenuItementerTextverifyRowscloseWithoutSaving)移到共享脚本中,代码会更少。而且大部分代码都是直接从记录的测试中复制过来的,并在某些情况下进行了参数化。

这足以体现出为自动测试系统(AUT)编写测试脚本的技巧。请注意,Squish提供了比这里所用的功能多得多的功能(所有这些都在API参考手册工具参考手册中有详细说明)。此外,Squish还提供了访问AUT对象公共API的全部功能。

然而,测试用例的一个方面并不令人满意。尽管像我们这里这样做将测试数据嵌入是很合理的,但对于少量数据来说,这相当有限,特别是当我们想要使用大量测试数据时。此外,我们并未测试添加到表中的任何数据是否有正确地结束在表中。在下一节中,我们将创建这个测试的另一个版本,但这次我们将从外部数据源中获取数据,并检查实际添加到表中的数据是否正确。

创建数据驱动测试

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

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

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

对于addressbook.tcl应用程序,我们想要导入MyAddresses.tsv数据文件。要这样做,我们必须首先点击文件 > 导入测试资源来弹出导入Squish资源对话框。在对话框内部,点击浏览按钮来选择要导入的文件——本例中为MyAddresses.tsv。确保导入为组合框设置为“测试数据”。默认情况下,squishide只会导入当前测试用例的测试数据,但我们希望测试数据对所有测试套件的测试用例都可用:为此,请勾选复制到测试套件以共享单选按钮。现在点击完成按钮。您现在可以在测试套件资源视图(在测试数据选项卡中)中看到文件列表,如果您点击文件名称,它将在一个编辑视图中显示。截图显示了添加测试数据后的Squish。

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

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

"Squish with some imported test data"

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

我们唯一需要更改的是《main》函数,我们将不再是遍历硬编码的数据项,而是遍历数据集中的所有记录。我们还需要更新最后的预期行数,因为我们现在要添加更多的记录,并且我们还会添加一个函数来验证每一项新增记录。

proc main {} {
    startApplication "addressbook.tcl"
    invokeMenuItem "File" "New"
    verifyRows 0
    # Set a limit to avoid testing 100s of rows
    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 phone [testData field $record "Phone"]
        set email [testData field $record "Email"]
        set details [list $forename $surname $phone $email]
        addNameAndAddress $details
        checkNameAndAddress $record
        if {$row > $limit} {
            break
        }
    }
    verifyRows [expr $row + 1]
    closeWithoutSaving
}
def main():
    startApplication("tclsh addressbook.tcl")

    # Create new file
    clickButton(waitForObject(names.addressbook_tcl_toolBar_fileNew))

    # Verify row count
    verifyRows(0)

    # Insert rows from data
    limit = 10 # To avoid testing 100s of rows since that would be boring
    for record in testData.dataset("MyAddresses.tsv")[:limit]:
        forename = testData.field(record, "Forename")
        lastname = testData.field(record, "Surname")
        email = testData.field(record, "Email")
        phone = testData.field(record, "Phone")
        addNameAddress((forename, lastname, phone, email))
        checkNameAndAddress(record)

    # Verify row count
    verifyRows(limit)

    closeWithoutSaving()
function main()
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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 phone = testData.field(record, "Phone");
        var email = testData.field(record, "Email");
        addNameAndAddress(new Array(forename, surname, phone, email));
        checkNameAndAddress(record);
        if (row > limit)
            break;
    }
    verifyRows(row + 1);
    closeWithoutSaving();
}
sub main
{
    startApplication("addressbook.tcl");
    invokeMenuItem("File", "New");
    verifyRows(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 $phone = testData::field($record, "Phone");
        my $email = testData::field($record, "Email");
        addNameAndAddress($forename, $surname, $phone, $email);
        checkNameAndAddress($record);
        if ($row > $limit) {
            last;
        }
    }
    verifyRows($row + 1);
    closeWithoutSaving;
}
def main
    startApplication("addressbook.tcl")
    invokeMenuItem("File", "New")
    verifyRows(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(record)
        break if row > limit
        rows += 1
    end
    verifyRows(rows + 1)
    closeWithoutSaving
end

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

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

proc checkNameAndAddress {record} {
    set columns [llength [testData fieldNames $record]]
    for {set column 0} {$column < $columns} {incr column} {
        set expected_text [testData field $record $column]
        waitForObject $names::addressbook_tcl_view_tree_2
        # New items are always inserted before the current one, so the row is always 0
        set cell [toString [invoke tcleval ".view.tree cellindex 0,$column"]]
        set actual_text [invoke tcleval ".view.tree cellcget $cell -text"]
        test compare $expected_text $actual_text
    }
}
def checkNameAndAddress(record):
    for column in range(len(testData.fieldNames(record))):
        expected_text = testData.field(record, column)
        waitForObject(names.addressbook_tcl_view_tree_body)
        # New items are always inserted before the current one, so the row is always 0
        actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,%d] -text" % column)
        test.compare(expected_text, actual_text)
function checkNameAndAddress(record)
{
    for (var column = 0; column < testData.fieldNames(record).length; ++column) {
        var expected_text = testData.field(record, column);
        waitForObject(names.addressbookTclViewTree);
        var actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0," + column + "] -text");
        test.compare(expected_text, actual_text);
    }
}
sub checkNameAndAddress
{
    my $record = shift;
    my @columnNames = testData::fieldNames($record);
    for (my $column = 0; $column < scalar(@columnNames); $column++) {
        my $expected_text = testData::field($record, $column);
        waitForObject($Names::addressbook_tcl_view_tree);
        # New items are always inserted before the current one, so the row is always 0
        my $actual_text = tcleval(".view.tree cellcget [.view.tree cellindex 0,$column] -text");
        test::compare($expected_text, $actual_text);
    }
}
def checkNameAndAddress(record)
    for column in 0...TestData.fieldNames(record).length
        actual_text = tcleval(
        ".view.tree cellcget [.view.tree cellindex 0,#{column}] -text")
        Test.compare(actual_text, TestData.field(record, column))
    end
end

此函数访问表的第1行并提取其所有列的值。我们使用 Squish 的《SequenceOfStrings testData.fieldNames(record)》函数来获取列数,然后使用《Boolean test.compare(value1, value2)》函数检查表中每个值是否与我们使用的测试数据相同。注意,对于这个特定的测试,我们始终将新行插入到表的开始部分。这种效果是,每个新的名称和地址总是作为第一行添加,这就是为什么我们将行列硬编码为0。

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

"Squish after a successful data-driven test run"

学习更多

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

API参考》和《工具参考》提供了 Squish 测试 API 的完整细节和它提供的许多函数,以使测试尽可能简单和高效。值得阅读《如何创建测试脚本》和《如何测试应用程序 - 特定细节》,同时浏览《API参考》和《工具参考》。你投入的时间将得到回报,因为你将了解 Squish 提供的功能,并可以避免重复发明早已可用的东西。

SQUISHDIR/examples/tk 中提供了更多的 Tk 例子应用程序及其相应的测试。

©2024 Qt 公司有限公司。本文件中包含的文档贡献是各自所有者的版权。
本文档所提供的文档在Free Software Foundation公布的GNU自由文档许可证的条款下授权使用,版本为1.3。
Qt及其相关标志是芬兰Qt公司以及全世界其他国家的商标。所有其他商标均为其各自所有者的财产。