Android Squish教程

了解如何测试Android应用程序。

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

Squish附带IDE和命令行工具。使用squishide是开始的最简单和最好方法,但一旦建立了很多测试,您可能希望自动化它们。例如,每晚运行回归测试套件。因此,了解如何使用可以从批处理文件或Shell脚本运行命令行工具是有价值的。

注意:如果您需要一些视频指导,可以在Qt Academy Qt Academy上找到一项关于Squish基本操作的45分钟在线课程

我们将测试一个非常简单的地址簿应用程序。用户可以通过所谓的活动,通过按钮添加地址,或者从活动菜单()选择示例数据来加载一些示例地址。当轻触现有地址时,用户可以编辑地址,或者从菜单中删除地址。尽管应用程序非常简单,但它具有您可能希望在自己的测试中使用的所有标准功能,包括菜单、列表、弹出对话框、行编辑和按钮。一旦您知道了如何测试这些用户界面元素,您就能将相同的原理应用到测试您自己应用程序中出现的元素,而这些元素在教程中没有使用,例如旋转框和日期时间控件。有关完整示例,请参阅如何创建测试脚本。要了解如何进行指南,请参阅如何测试Android应用程序

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

"The AddressBook for Android example"

该应用程序(即AUT—正在测试的应用程序)可以在Squish示例中找到,位置在SQUISHDIR/examples/android/AddressBook/AddressBook-debug.apk。以下部分中我们将讨论的测试位于文件夹中,例如,使用Python编写的测试版本的测试位于SQUISHDIR/examples/android/AddressBook/suite_py,而用其他语言编写的测试位于类似命名的子文件夹中。

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

Squish概念

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

为了测试Android AUT,需要以下内容:

  1. 一个可由<+a href="https://android-docs.cn/tools/adb" translate="no">adb查看的运行中的Android系统。
  2. 要测试的应用程序——称为正在测试的应用程序(AUT)。
  3. 一个测试脚本,该脚本会测试AUT。

注意:对于Android设备,确保设备连接到您的PC后启用USB调试。这通常需要首先在设备上启用开发者选项,然后再在设置下看到该选项。

Squish运行一个小的服务器,称为squishserver,用于处理AUT(自动测试工具)和测试脚本之间的通信。测试脚本由squishrunner工具执行,然后该工具连接到squishserver。squishserver在设备上启动被instrumented的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。在某些Java工具包中,等效的是Component

使应用程序可测试

当测试Android AUT时,Instrument and Deploy步骤会在应用程序中嵌入额外的代码以包含Squish钩子。这个过程包括解压包、添加类,然后再次压缩。更多细节可以在使Android应用程序可测试中找到。

创建测试套件

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

在这里,以及在整个教程中,我们将首先描述如何使用squishide来进行操作,然后将是命令行用户的信息。

开始之前

在继续之前,请确保在运行Squish的系统上已经安装了Java JDK,版本7或更高。

如果您在Android虚拟设备(AVD)上进行测试,您需要已安装Android Developer Studio,并从AVD Manager设置一个设备。否则,您应该连接通过USB的物理Android设备,并启用USB调试。无论如何,Android设备都应该出现在其自己的组合框中,在squishide的测试套件视图的Test Suites组合框下。

请确保没有其他可以访问模拟器或设备的Android开发环境正在运行。这包括带有ADT插件的Eclipse和Android Developer Studio。这些工具会防止Squish访问AUT(我们要测试的应用)。

squishide 创建测试套件

启动 squishide,通过单击或双击squishide图标,从任务栏菜单打开squishide,或在命令行中执行squishide,您可以选择任何您喜欢和适合您使用的平台的方法。Squish启动后,您可能会看到一个 欢迎页面。在右上角单击 工作台 按钮关闭它。然后,squishide将看起来 类似 截图,但可能因为窗口系统、颜色、字体和主题的不同而略有不同。

"The Squish IDE with no Test Suites"

启动Squish后,单击 文件 > 新建测试套件 以打开 新建测试套件 向导。

"Name & Directory page"

输入您的测试套件名称并选择您希望测试套件存储的文件夹。在截图中,我们称测试套件为 suite_py,并将其放在一个 addressbook 文件夹内。当然,您可以根据喜好选择任何名称和文件夹。详细信息填写完毕后,单击 下一步 继续到工具包(或脚本语言)页面。

"Toolkit page"

如果出现此向导页面,请单击AUT使用的工具包。对于此示例,我们必须单击 Android,因为我们正在测试Android应用程序。然后单击 下一步 进入 脚本语言 页面。

"Scripting Language page"

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

"AUT page"

本教程使用的是 com.froglogic.addressbook,一个非常简单的地址簿应用。

"The Instrument and Deploy an application dialog"

单击 新建,并填写缺失的字段。单击 浏览,并选择 SQUISHDIR/examples/android/AddressBook/AddressBook-debug.apk。此文件包含应用 com.froglogic.addressbook

如果您JDK的bin目录在您的PATH中,您可以将 JDK路径 留空。一般来说,在Windows上进行工作的用户必须指定此目录,而在Linux或Mac用户可以留空此字段,但必须确保已安装Java 7或更高版本。

您应该会看到列出的一个或多个Android设备。选择一个或多个设备,然后按 仪器和部署

"AUT page with package"

AUT部署后,向导将在AUT组合框中显示 com.froglogic.addressbook

当您单击 完成 时,Squish将在名为 suite_suiteName/ 的子文件夹中创建一个文件夹,并在该文件夹中创建一个名为 suite.conf 的文件,其中包含测试套件的配置详细信息。Squish还会将AUT注册到squishserver。然后向导关闭,squishide 将类似于下面的截图。

"Squish IDE with the suite_py test suite"

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

从命令行创建测试套件

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

  1. 创建一个新的目录以存放测试套件。目录名称应以 suite 开头。在本例中,我们为 Python 测试创建了 suite_py 目录。
  2. 在套件子目录中创建一个名为 suite.conf 的纯文本文件(ASCII 或 UTF-8 编码)。这是测试套件的配置文件,至少必须标识出自动测试工具(AUT)、测试使用的脚本语言以及 AUT使用的包装器(即,GUI 工具包或库)。文件的格式为 键 = 值,每行一个 键值对。例如
    AUT            = com.froglogic.addressbook
    LANGUAGE       = Python
    WRAPPERS       = Android
    OBJECTMAPSTYLE = script

    Android 程序的 AUT 是应用程序的完整的 Java 语言风格的包名。以下命令可以在 Squish 服务器以及您的 Android 模拟器或设备运行时获取所有可能的 AUT 列表:

    squishrunner --info androidInstrumentation

    根据 Squish 的安装方式,语言可以设置为 JavaScript、Python、Perl、Ruby 或 Tcl。

  3. 在 Android 中安装 Squish 详细说明了如何详细地进行参数化和部署 APK 文件。基本上,从 Squish 目录下,在 Windows 上运行:
    bin\apk-tool -j "C:\Program Files\Java\jdkx.y.z" -pkg "<your-apk>" -o "%TEMP% -d <device>
    

    以及 Linux 或 Mac 上

    bin/apk-tool -pkg "<your-apk>" -o /tmp -d <device>

    其中 device 是目标设备或模拟器。运行以下命令以获取所有已连接的设备和模拟器列表:

    squishrunner --info androidDevices

我们现在可以为我们的第一个测试录制。

录制测试和验证点

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/android/AddressBook/suite_py目录内,我们有tst_general目录。
  2. 在测试用例目录内创建一个名为test.py(如果您使用的是JavaScript脚本语言,则为test.js,以及其他语言也类似)的文件。

录制我们的第一个测试

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

  1. 加载地址。
  2. 编辑第二个地址条目的姓氏。
  3. 导航到第一个地址并删除它。

我们现在可以录制我们的第一个测试。单击测试用例列表中显示的tst_general测试用例右侧的“录制”()按钮。这将使Squish运行AUT,以便我们可以与之交互。一旦AUT运行,请执行以下操作——不要担心它用时有多长,因为Squish不会记录空闲时间

  1. 在设备上的菜单按钮上轻触,然后轻触演示数据
  2. 轻触第二行,编辑地址页面将打开。然后在文本右边某处轻触第二行,按下删除键几次并输入“Doe”。不必担心输入错误——像正常那样按删除键删除并修正它们。最后,轻触保存按钮——如果屏幕键盘挡路,只需按下返回按钮。现在第二行应该有您输入的修改后的“Doe”姓氏。
  3. 现在轻触第一行。在编辑地址页面上,轻触菜单按钮,然后轻触删除地址,然后轻触消息框中的删除按钮。第一行应该消失了,因此修改后的“Doe”条目现在应该是第一个。
  4. 单击Squish中控制栏窗口(从左侧第二个按钮)的验证工具栏按钮,然后选择属性

    {}

    这将使squishide出现。在应用对象视图中,展开AddressBook对象,重复操作,直到展开第一个ListView下的LinearLayout对象。对于本教程使用的模拟器,还需要展开FrameLayout对象和LinearLayout对象。单击Doe对象,使其属性出现在属性查看器中,然后检查text属性的复选框。这将插入一个验证点squishide。最后,单击底部验证点创建器查看器中的保存并插入验证按钮,将第一行的姓氏验证自动插入到录制脚本中。(请参阅下面的截图。)一旦插入验证点,squishide的窗口将再次隐藏,控制栏窗口和AUT将重新出现在视图中。

  5. 我们已完成测试,因此请在AUT中按菜单按钮,然后轻触退出。最后,如果控制栏仍然存在,请点击其中的停止录制按钮。

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

"The recorded tst_general test"

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

现在我们已经记录了测试,我们可以回放它,即运行它。这本身就是有用的,因为如果回放失败,这可能会意味着应用程序已损坏。此外,我们在回放时将检查我们插入的验证(如上图所示)。

在测试录制过程中插入验证点非常方便。这里我们只插入了一个,但我们可以根据需要反复插入任意数量的验证点。然而,有时我们可能会忘记插入一个验证点,或者后来我们可能想插入一个新的验证点。我们可以在下一个部分中轻松地将额外的验证点插入到记录的测试脚本中,正如我们将要看到的插入更多验证点

在继续之前,我们将探讨如何从命令行录制测试。然后我们将看到如何运行测试,并查看Squish为录制测试生成的部分代码,并讨论其一些功能。

从命令行记录测试

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

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

squishrunner --testsuite suite_AddressBook_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_AddressBook_py --testcase tst_general --local

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

检验生成的代码

如果您查看截图中的代码(或下面的代码片段),您将看到它由大量Object waitForObject(objectOrName)调用组成,这些调用是其他调用(如openMenu(objectOrName)tapMenuItem(objectOrName)tapObject(objectOrName)type(objectOrName, text))的参数。`Object waitForObject(objectOrName)函数等待直到GUI对象准备好交互(即可见并启 用),然后跟着某个与对象互动的函数。典型的交互是激活(弹出)菜单、轻触菜单选项或按钮或输入一些文本

有关Squish脚本命令的完整概览,请参阅《如何创建测试脚本》、《如何测试应用程序 - 特殊细节》、《API参考手册》和《工具参考手册》。

对象通过Squish生成的名称进行识别。有关详细信息,请参见《如何识别和访问对象》。

生成的代码大约有23行。以下是一个摘录,它只说明了Squish如何记录轻按地址输入、更改姓氏以及最终轻按“保存”来关闭页面并更新表的方式。

注意:虽然截图只展示了Python测试套件的工作状态,但对于本教程中引用的代码片段和整个教程,我们都展示了Squish支持的脚本语言代码。在实际情况中,您通常会只使用其中之一,所以您只需查看您感兴趣的脚本语言片段,而无需关注其他语言。

    tapObject(waitForObject(names.abdulWahhabText), 34, 10);
    tapObject(waitForObject(names.editAddressSurnameEdit), 150, 15);
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressSurnameEdit), "<Backspace>");
    type(waitForObject(names.editAddressForenameEdit), "Doe");
    tapObject(waitForObject(names.editAddressSaveButton), 20, 10);
    tapObject(waitForObject($Names::abdul_wahhab_text), 34, 10);
    tapObject(waitForObject($Names::edit_address_surname_edit), 150, 15);
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_surname_edit), "<Backspace>");
    type(waitForObject($Names::edit_address_forename_edit), "Doe");
    tapObject(waitForObject($Names::edit_address_save_button), 20, 10);
    tapObject(waitForObject(names.abdul_Wahhab_Text), 34, 10)
    tapObject(waitForObject(names.edit_Address_Surname_Edit), 150, 15)
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(names.edit_Address_Forename_Edit), "Doe")
    tapObject(waitForObject(names.edit_Address_Save_Button), 20, 10)
    tapObject(waitForObject(Names::Abdul_Wahhab_Text), 34, 10)
    tapObject(waitForObject(Names::Edit_Address_Surname_Edit), 150, 15)
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Surname_Edit), "<Backspace>")
    type(waitForObject(Names::Edit_Address_Forename_Edit), "Doe")
    tapObject(waitForObject(Names::Edit_Address_Save_Button), 20, 10)
    invoke tapObject [waitForObject $names::Abdul_Wahhab_Text] 34 10
    invoke tapObject [waitForObject $names::Edit_Address_Surname_Edit] 150 15
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] "<Backspace>"
    invoke type [waitForObject $names::Edit_Address_Forename_Edit] "Doe"
    invoke tapObject [waitForObject $names::Edit_Address_Save_Button] 20 10

正如您所看到的,测试人员使用触摸操作将输入焦点设置到编辑字段上。如果测试人员通过使用硬件键盘的箭头键移动焦点,结果将是相同的,但当然Squish会记录实际执行的动作。

代码片段中没有明确的时间延迟。要强制延迟,请使用Squish的snooze(seconds)函数。

This is because the Object waitForObject(objectOrName) function delays until the object it is given is ready, thus allowing Squish to run as fast as the GUI toolkit can cope with, but no faster.

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

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

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

插入附加的验证点

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

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

实际上,Scriptified 特性验证仅仅是调用 布尔测试.compare(value1, value2) 函数,带有两个参数——特定对象特定特性的值和一个预期值。我们可以在记录的脚本或手写的脚本中手动插入调用 布尔测试.compare(value1, value2) 函数,或者我们可以使用 squishide 让 Squish 自动插入它们。在前一节中,我们展示了如何使用 squishide 在录制过程中插入验证。在这里,我们首先将展示如何使用 squishide 将验证插入现有的测试脚本中,然后我们将展示如何手动插入一个验证。

注意:为了准备下一步,创建一个名为 新脚本测试用例 (),代码为 add_address,然后 录制 () 添加示例条目(我们选取的名称为 Zikra Glen),然后退出 AUT。我们的后续练习将基于这个脚本。

要使用 squishide 插入一个验证点,我们首先在每个要验证的位置在脚本中设置一个断点。

"The tst_add_address test case with a breakpoint"

如上述屏幕截图所示,我们在第 16 行设置了断点。只需双击,或在编辑器中列号旁边的空白处(即侧栏)右击,选择 添加断点 菜单项即可完成此操作。我们选择这一行,因为它紧随将地址保存的脚本行之后,因此在这一点(在调出退出应用程序的菜单之前),第一个地址应该是“Zikra Glen”。如果你的测试用例录制方式不同,比如,而不是通过点击菜单项而是使用键盘快捷键,你的行号可能不同。

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

视角和视图

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

squishide 提供以下视角:

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

插入验证点

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

要插入属性验证点,我们首先必须确定(通过选择)包含所关心的属性的哪个对象,然后我们必须检查所关心的属性。我们可以展开 应用对象 视图中的项目,直到找到要验证的对象,或者使用 对象选择器)在GUI中进行可视化查找。在这个例子中,我们想验证第一行的文本,所以我们需要展开 AddressBook 项目及其子项目,直到找到 ListView,然后在其中查找我们感兴趣的对象。一旦我们点击了 Zikra 对象,它的属性就像截图显示的那样在 属性视图 中显示。

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

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

在这里,我们可以看到这个 TextView 项目的 text 属性的值是 "Zikra"。为了确保每次测试运行时都验证这一点,请单击 应用对象视图 中的此 TextView 项目以显示其属性,然后单击 text 属性以检查其复选框。检查后,验证点创建视图 就会像截图显示的那样出现。

"Choosing a property value to verify"

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

向下滚动并单击 应用对象视图 中的 "Glen" TextView 项目;然后单击其 text 属性。现在两个验证都会出现在 验证点创建视图 中,如下截图所示。

"Choosing several property values to verify"

我们已经说明了我们期望这些属性具有显示的值,即名字为 "Zikra" 和姓氏为 "Glen"。我们 必须 单击 插入 按钮,实际上插入验证点,所以现在就做吧。

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

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

"Newly inserted verification points"

这些特定的验证点生成两个测试,比较新插入条目的名字和姓氏。

插入验证点的另一种方法是将其以代码的形式插入。从理论上讲,我们可以在现有脚本中的任何地方添加我们自己的对Squish测试函数的调用,例如 Boolean test.compare(value1, value2)Boolean test.verify(condition)。在实践中,最好确保Squish首先知道我们想要验证的对象,这样它才能在运行测试时找到它们。这涉及一个与使用 squishide 相似的过程。首先,我们在要添加验证的地方设置一个断点。然后,运行测试脚本直到它停止。接下来,我们在 应用程序对象视图 中导航,直到找到我们想要验证的对象。这时,右键单击我们感兴趣的对象并点击上下菜单一 添加到对象图 动作是明智的。这将确保Squish可以访问该对象。然后再次右键单击并选择上下菜单一 复制符号名 动作——这给我们提供了Squish将用于识别对象的名称。现在我们可以编辑测试脚本,添加我们自己的验证并完成执行。(别忘了在不再需要时禁用断点。)

虽然我们可以编写与自动生成的代码完全相同的风格的测试脚本代码,但使用稍微不同的风格通常更清晰、更容易,如下文所述。

对于我们的手动验证,我们希望在插入条目后检查列表中存在的地址数量。截图显示了我们所输入的用于验证的两行代码,以及运行测试脚本的结果。

{Manually entered verification points"

在手动编写脚本时,我们使用Squish的 test 模块的函数在测试脚本执行过程中某些点验证条件。如图所示,我们首先获取对感兴趣对象的引用。在手动编写的测试脚本中使用 Object waitForObject(objectOrName) 函数是标准做法。此函数等待对象可用(例如,可见并启用),然后返回对其的引用。(否则它会超时并引发可捕获的异常。)然后我们将使用此引用来访问项的属性——在这种情况下是 ListView 的 rowCount——然后使用 test.compare 对其值与我们期望它是什么进行验证。

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

有关手动编写的代码示例,请参阅《手动创建测试》,《如何创建测试脚本》和《如何测试应用程序 - 详细说明》。

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

测试结果

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

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

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

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

手动创建测试

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

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

我们可以通过点击对象图工具栏按钮(),进入对象图编辑器(也可参见对象视图)。在此列出的每个与Squish交互的应用程序对象,可以作为一个顶级对象或子对象(视图呈现为树模式)。我们可以通过在所需对象上右键单击,然后在上下文中选择复制符号名称操作来检索Squish在已录制脚本中使用的符号名称。这在我们需要修改现有测试脚本或从头创建测试脚本时非常有用,我们将在教程的后续部分看到这一点。

"Object Map"

或者,我们也可以通过右键单击感兴趣的物体,然后点击上下文菜单中的复制真实名称使用真实名称。对于这个List对象,“真实名称”是本地脚本语言键值的映射。由于对象引用(例如,容器)可以用符号名称或真实名称作为值,因此以下三个waitForObject调用在测试套件的上下文中是等价的。

waitForObject(names.address_Book_List)
waitForObject({"container": names.address_Book_Activity, "type": "List", "visible": True})
waitForObject({"container": {"text": "Address Book", "type": "Activity", "visible": True}, "type": "List", "visible": True})
waitForObject(names.addressBookList);
waitForObject({"container": names.addressBookActivity, "type": "List", "visible": true});
waitForObject({"container": {"text": "Address Book", "type": "Activity", "visible": true}, "type": "List", "visible": true});
waitForObject($Names::address_book_list);
waitForObject({"container" => $Names::address_book_activity, "type" => "List", "visible" => "true"});
waitForObject({"container" => {"text" => "Address Book", "type" => "Activity", "visible" => "true"}, "type" => "List", "visible" => "true"});
waitForObject(Names::Address_Book_List))
waitForObject({:container => Names::Address_Book_Activity, :type => "List", :visible => true}))
waitForObject({:container => {:text => "Address Book", :type => "Activity", :visible => true}, :type => "List", :visible => true}))
waitForObject $names::Address_Book_List
waitForObject [::Squish::ObjectName container $names::Address_Book_Activity type List visible true]
waitForObject [::Squish::ObjectName container [::Squish::ObjectName text {Address Book} type Activity visible true] type List visible true]

这在大批量动态创建对象名称时很有用。直接使用真实名称时,不需要从对象图中获取条目。更多详情,请参见如何访问命名对象

修改和改写已记录的测试

假设我们想通过添加三个新地址来测试AUT的“添加”功能。当然,我们可以记录这样的测试,但使用代码来完成所有操作也是一样的简单。我们需要测试脚本来执行的步骤是:

  1. 点击“添加地址”按钮
  2. 填写字段
  3. 点击“保存”按钮

我们还想在开始时验证没有数据行,在结束时有三个行。我们将逐步重构,尽可能让我们的代码整洁且模块化。

让我们从之前创建的“tst_add_address”脚本开始,将其转换为接收字段值作为参数的函数。

def addNameAndAddress(fields):
    forname,surname,email,phone = fields
    tapObject(waitForObject(names.address_Book_Add_Address_Button))
    tapObject(waitForObject(names.edit_Address_Forename_Edit))
    type(waitForObject(names.edit_Address_Forename_Edit), forname)
    tapObject(waitForObject(names.edit_Address_Surname_Edit))
    type(waitForObject(names.edit_Address_Surname_Edit), surname)
    tapObject(waitForObject(names.edit_Address_Phone_Edit))
    type(waitForObject(names.edit_Address_Phone_Edit), phone)
    tapObject(waitForObject(names.edit_Address_Email_Edit))
    type(waitForObject(names.edit_Address_Email_Edit), email)
    tapObject(waitForObject(names.edit_Address_Save_Button))
function addNameAndAddress(fields)
{
    tapObject(waitForObject(names.addressBookAddAddressButton));
    tapObject(waitForObject(names.editAddressForenameEdit));
    type(waitForObject(names.editAddressForenameEdit), fields[0]);
    tapObject(waitForObject(names.editAddressSurnameEdit));
    type(waitForObject(names.editAddressSurnameEdit), fields[1]);
    tapObject(waitForObject(names.editAddressPhoneEdit));
    type(waitForObject(names.editAddressPhoneEdit), fields[3]);
    tapObject(waitForObject(names.editAddressEmailEdit));
    type(waitForObject(names.editAddressEmailEdit), fields[2]);
    tapObject(waitForObject(names.editAddressSaveButton));
}
sub addNameAndAddress {
    my ($forname,$surname,$email,$phone) = @_;
    tapObject(waitForObject($Names::address_book_add_address_button));
    tapObject(waitForObject($Names::edit_address_forename_edit));
    type(waitForObject($Names::edit_address_forename_edit), $forname);
    tapObject(waitForObject($Names::edit_address_surname_edit));
    type(waitForObject($Names::edit_address_surname_edit), $surname);
    tapObject(waitForObject($Names::edit_address_phone_edit));
    type(waitForObject($Names::edit_address_phone_edit), $phone);
    tapObject(waitForObject($Names::edit_address_email_edit));
    type(waitForObject($Names::edit_address_email_edit), $email);
    tapObject(waitForObject($Names::edit_address_save_button));
}
def addNameAndAddress(fields)
    tapObject(waitForObject(Names::Address_Book_Add_Address_Button))
    tapObject(waitForObject(Names::Edit_Address_Forename_Edit))
    type(waitForObject(Names::Edit_Address_Forename_Edit), fields[0])
    tapObject(waitForObject(Names::Edit_Address_Surname_Edit))
    type(waitForObject(Names::Edit_Address_Surname_Edit), fields[1])
    tapObject(waitForObject(Names::Edit_Address_Phone_Edit))
    type(waitForObject(Names::Edit_Address_Phone_Edit), fields[3])
    tapObject(waitForObject(Names::Edit_Address_Email_Edit))
    type(waitForObject(Names::Edit_Address_Email_Edit), fields[2])
    tapObject(waitForObject(Names::Edit_Address_Save_Button))
end
proc addNameAndAddress {fields} {
    invoke tapObject [waitForObject $names::Address_Book_Add_Address_Button]
    invoke tapObject [waitForObject $names::Edit_Address_Forename_Edit]
    invoke type [waitForObject $names::Edit_Address_Forename_Edit] [lindex $fields 0]
    invoke tapObject [waitForObject $names::Edit_Address_Surname_Edit]
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] [lindex $fields 1]
    invoke tapObject [waitForObject $names::Edit_Address_Phone_Edit]
    invoke type [waitForObject $names::Edit_Address_Phone_Edit] [lindex $fields 3]
    invoke tapObject [waitForObject $names::Edit_Address_Email_Edit]
    invoke type [waitForObject $names::Edit_Address_Email_Edit] [lindex $fields 2]
    invoke tapObject [waitForObject $names::Edit_Address_Save_Button]
}

接下来我们用来自main函数的列字段数组调用此函数。

import names

def main():
    startApplication("com.froglogic.addressbook")
    table = waitForObject(names.address_Book_List)
    test.verify(table.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(table.rowCount, len(data))
    openMenu(waitForObject(names.address_Book_Activity))
    tapMenuItem(waitForObject(names.address_Book_Activity), "Quit")
import * as names from 'names.js';

function main()
{
    startApplication("com.froglogic.addressbook");
    var table = waitForObject(names.addressBookList);
    test.verify(table.rowCount == 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.rowCount, data.length);

    // depending on the Android API version, either a menu or toolbar is shown
    var androidApiVersion = waitForObjectExists(names.addressBookActivity).nativeObject.getClass().forName("android.os.Build$VERSION").newInstance().SDK_INT;
    if (androidApiVersion < 21) {
      openMenu(waitForObject(names.addressBookActivity));
      tapMenuItem(waitForObject(names.addressBookActivity), "Quit");
    } else {
      tapObject(waitForObject(names.mainToolbarButton));
      tapObject(waitForObjectItem(names.mainToolbar, "Quit"));
    }
}
require 'names.pl';

sub main() {
    startApplication("com.froglogic.addressbook");
    my $table = waitForObject($Names::address_book_list);
    test::verify($table->rowCount == 0);
    my @data = (['Andy', 'Beach', '[email protected]', '555 123 6786'],
            ['Candy', 'Deane', '[email protected]', '555 234 8765'],
            ['Ed', 'Fernleaf', '[email protected]', '555 876 4654']);
    foreach $line (@data) {
        addNameAndAddress(@$line);
    }
    test::compare($table->rowCount, scalar(@data));
    openMenu(waitForObject($Names::address_book_activity));
    tapMenuItem(waitForObject($Names::address_book_activity), "Quit");
}
require 'names';
require 'squish'
include Squish

def main
    startApplication("com.froglogic.addressbook")
    table = waitForObject(Names::Address_Book_List)
    Test.verify(table.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 |address|
        addNameAndAddress(address)
    end
    Test.compare(table.rowCount, data.length)
    openMenu(waitForObject(Names::Address_Book_Activity))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Quit")
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "com.froglogic.addressbook"
    set table [waitForObject $names::Address_Book_List]
    test compare [property get $table 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 $table rowCount] [llength $data]
    invoke openMenu [waitForObject $names::Address_Book_Activity]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Quit"
}

然而,测试用例的一个方面并不令人满意。虽然我们在这里嵌入测试数据对于小量数据是合理的,但它相对有限,特别是当我们希望使用大量测试数据时。在下一节中,我们将创建这个测试的新版本,但我们这次将从外部数据源拉取数据。

创建数据驱动测试

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

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

对于地址簿应用程序,我们想要导入 MyAddresses.tsv 数据文件(此文件的副本位于 SQUISHDIR/examples/android/AddressBook/suite_xy/shared/testdata)。为此,我们单击 文件 > 导入测试资源 以弹出 导入Squish资源对话框。在对话框内部,单击 浏览 按钮选择要导入的文件——《在这种情况下为 MyAddresses.tsv》。确保将 导入为 组合框设置为“测试数据”。默认情况下,squishide 将仅导入当前测试用例的测试数据,但我们希望测试数据对所有测试套件的测试用例都可用:为此,选中 将文件复制到共享测试套件中 单选按钮。现在单击 完成。你现在可以在 测试套件资源 视图(在 测试数据 选项卡中)中看到该文件列出,如果你单击文件名,它将显示在一个 编辑视图 中。截图显示了添加测试数据后的Squish。

导入测试数据

要从 squishide 外部导入测试数据,请使用文件管理器,例如文件资源管理器,或控制台命令。在测试套件的目录内创建一个名为 shared 的目录。在其内部,创建另一个名为 testdata 的目录。然后,将数据文件(在这个例子中是 MyAddresses.tsv)复制到 shared/testdata 目录中。重启 squishide 后,你应该在 测试套件资源 视图 测试数据 选项卡中看到该数据文件。单击文件名称,可以在 编辑视图 中查看。

"Squish with some imported test data"

添加测试用例

尽管在实际情况下我们会修改 tst_add_address 测试用例以使用测试数据,但对于本教程的目的,我们将创建一个名为 tst_adding_data 的新测试用例,其是 tst_add_address 的副本,并修改以使用测试数据。

唯一需要更改的函数是 main,在这里,我们不是迭代硬编码的数据项,而是迭代数据集中的所有记录。我们还需要更新末尾的预期行数,因为现在我们添加了更多记录。

import names

def main():
    startApplication("com.froglogic.addressbook")
    table = waitForObject(names.address_Book_List)
    test.verify(table.rowCount == 0)
    limit = 10 # To avoid testing 100s of rows since that would be boring
    for row, record in enumerate(testData.dataset("MyAddresses.tsv")):
        fields = testData.field(record, "Forename"), testData.field(record, "Surname"), testData.field(record, "Email"), testData.field(record, "Phone")
        addNameAndAddress(fields)
        if row > limit:
            break
    test.compare(table.rowCount, row+1)
import * as names from 'names.js';

function main()
{
    startApplication("com.froglogic.addressbook");
    var table = waitForObject(names.addressBookList);
    test.verify(table.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 line = [testData.field(record, "Forename")
                   , testData.field(record, "Surname")
                   , testData.field(record, "Email")
                   , testData.field(record, "Phone")];
        addNameAndAddress(line);
        if (row > limit)
            break;
    }
    test.compare(table.rowCount, row+1);
    openMenu(waitForObject(names.addressBookActivity));
    tapMenuItem(waitForObject(names.addressBookActivity), "Quit");
}
require 'names.pl';

sub main() {
    startApplication("com.froglogic.addressbook");
    my $table = waitForObject($Names::address_book_list);
    test::verify($table->rowCount == 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 @line = ( testData::field($record, "Forename")
                   , testData::field($record, "Surname")
                   , testData::field($record, "Email")
                   , testData::field($record, "Phone") );
        addNameAndAddress(@line);
        if ($row > $limit) {
            last;
        }
    }
    test::compare($table->rowCount, $row+1);
    openMenu(waitForObject($Names::address_book_activity));
    tapMenuItem(waitForObject($Names::address_book_activity), "Quit");
}
require 'squish'
require 'names';
include Squish

def main
    startApplication("com.froglogic.addressbook")
    table = waitForObject(Names::Address_Book_List)
    Test.verify(table.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|
        line = [TestData.field(record, "Forename"),
                TestData.field(record, "Surname"),
                TestData.field(record, "Email"),
                TestData.field(record, "Phone")]
        addNameAndAddress(line)
        break if row > limit
        rows += 1
    end
    Test.compare(table.rowCount, rows+1)
    openMenu(waitForObject(Names::Address_Book_Activity))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Quit")
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "com.froglogic.addressbook"
    set table [waitForObject $names::Address_Book_List]
    test compare [property get $table 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 fields [list \
                    [testData field $record "Forename"]  \
                    [testData field $record "Surname"]   \
                    [testData field $record "Email"]     \
                    [testData field $record "Phone"]]
        addNameAndAddress $fields
        if {$row > $limit} {
            break
        }
    }
    test compare [property get $table rowCount] [expr $row+1]
    invoke openMenu [waitForObject $names::Address_Book_Activity]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Quit"
}

使用Android原生API

在本节中,我们快速查看nativeObject属性,并使用它重写了一个滚动通过地址演示列表的已记录脚本。该属性允许访问Squish用户界面对象的底层Java对象。这些原生对象的属性和方法在脚本使用时动态创建。有关它们的文档,请参考在线 Android开发者参考

当在一个在滚动列表时才可见的项目上进行测试录制时,我们可能会得到一个包含 touchAndDrag(objectOrName, x, y, dx, dy) 函数的脚本。例如,这里有一个简短的录制

function main() {
    startApplication("com.froglogic.addressbook");
    openMenu(waitForObject(names.addressBookActivity));
    tapMenuItem(waitForObject(names.addressBookActivity), "Demo Data");
    touchAndDrag(waitForObject(names.coonsText), 49, 6, 32, -451);
    touchAndDrag(waitForObject(names.mccullaghText), 105, 19, 13, -252);
    touchAndDrag(waitForObject(names.harrietText), 154, 15, -40, 365);
    touchAndDrag(waitForObject(names.dewarText), 149, 28, -51, 266);
    openMenu(waitForObject(names.addressBookList));
    tapMenuItem(waitForObject(names.addressBookActivity), "Quit");
}
sub main
{
    startApplication("com.froglogic.addressbook");
    openMenu(waitForObject($Names::address_book_activity));
    tapMenuItem(waitForObject($Names::address_book_activity), "Demo Data");
    touchAndDrag(waitForObject($Names::boardus_text), 71, 10, -26, 229);
    touchAndDrag(waitForObject($Names::puckett_text), 61, 25, 14, -257);
    touchAndDrag(waitForObject($Names::burnand_text), 61, 12, 14, -240);
    touchAndDrag(waitForObject($Names::dyson_text), 67, 32, -29, 253);
    touchAndDrag(waitForObject($Names::case_text), 71, 5, 3, 196);
    openMenu(waitForObject($Names::address_book_list));
    tapMenuItem(waitForObject($Names::address_book_activity), "Quit");
}
def main():
    startApplication("com.froglogic.addressbook")
    openMenu(waitForObject(names.address_Book_Activity))
    tapMenuItem(waitForObject(names.address_Book_Activity), "Demo Data")
    touchAndDrag(waitForObject(names.atkinson_Text), 39, 9, 8, -198)
    touchAndDrag(waitForObject(names.harriet_Text), 69, 19, 16, -195)
    touchAndDrag(waitForObject(names.address_Book_List), 582, 556, 5, -251)
    touchAndDrag(waitForObject(names.burnand_Text), 49, 4, -24, 250)
    touchAndDrag(waitForObject(names.address_Book_List), 554, 558, 23, -247)
    openMenu(waitForObject(names.address_Book_List))
    tapMenuItem(waitForObject(names.address_Book_Activity), "Quit")
def main
    startApplication("com.froglogic.addressbook")
    openMenu(waitForObject(Names::Address_Book_Activity))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Demo Data")
    touchAndDrag(waitForObject(Names::Wasling_Text), 55, 18, 1, -221)
    touchAndDrag(waitForObject(Names::Hullson_Text), 53, 28, 3, -144)
    touchAndDrag(waitForObject(Names::Munford_Text), 62, 7, -12, 323)
    touchAndDrag(waitForObject(Names::Puckett_Text), 55, 6, 2, -170)
    openMenu(waitForObject(Names::Address_Book_List))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Quit")
end
proc main {} {
    startApplication "com.froglogic.addressbook"
    invoke openMenu [waitForObject $names::Address_Book_Activity]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Demo Data"
    invoke touchAndDrag [waitForObject $names::Puckett_Text] 68 4 -14 -175
    invoke touchAndDrag [waitForObject $names::Selby_Text] 65 10 -11 -196
    invoke touchAndDrag [waitForObject $names::Coons_Text] 58 30 8 202
    invoke touchAndDrag [waitForObject $names::Grieve_Text] 37 13 -6 182
    invoke openMenu [waitForObject $names::Address_Book_List]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Quit"
}

虽然只要演示列表不变,这段重放效果很好,但在重播时速度较慢。此外,touchAndDrag(objectOrName, x, y, dx, dy) 函数的起始点对象在重放使用具有较小垂直分辨率的设备或模拟器时可能不存在。更健壮的方法是遍历列表项目,将该列表滚动到指定的项,然后点击该项。

"nativeObject property"

我们可以使用ListView方法的smoothScrollToPosition函数(https://android-docs.cn/reference/android/widget/AbsListView.html#smoothScrollToPosition(int))来编程滚动列表。为了找到要滚动到的地方,我们展示了三种不同的方法。

  1. 使用适配器
  2. 在滚动过程中修改对象层次结构
  3. 在滚动过程中搜索对象名称

更多有关使用nativeObject的内容,请参阅章节 如何使用nativeObject属性

使用列表的适配器

我们可以使用这个列表的适配器,因为适配器是一个SimpleAdapter。它包含了ListMap对象。

以下是一个使用这种方法并给定列表和待查找文本的函数示例

def scrollListToText1(list, text):
    adapter = list.nativeObject.adapter
    for i in range(adapter.getCount()):
        row = adapter.getItem(i)
        if (row.containsValue(text)):
            list.nativeObject.smoothScrollToPosition(i)
            break
function scrollListToText1(list, text) {
    var adapter = list.nativeObject.adapter;
    var total = adapter.getCount();
    for (var i = 0; i < total; ++i) {
        var row = adapter.getItem(i);
        if (row.containsValue(text)) {
            list.nativeObject.smoothScrollToPosition(i);
            break;
        }
    }
}
sub scrollListToText1 {
    my ($list, $text) = @_;
    my $adapter = $list->nativeObject->adapter;
    my $total = $adapter->getCount();
    for (my $i = 0; $i < $total; ++$i) {
        my $row = $adapter->getItem($i);
        if ($row->containsValue($text)) {
            $list->nativeObject->smoothScrollToPosition($i);
            last;
        }
    }
}
def scrollListToText1(list, text)
    adapter = list.nativeObject.adapter
    total = adapter.getCount();
    i = 0
    while i < total
        row = adapter.getItem(i)
        if (row.containsValue(text))
            list.nativeObject.smoothScrollToPosition(i)
            break
        end
        i += 1
    end
end
proc scrollListToText1 {lst text} {
    set adapter [property get [property get $lst nativeObject] adapter]
    set total [invoke $adapter getCount]
    for {set i 0} {$i < $total} {incr i} {
        set row [invoke $adapter getItem $i]
        if {[invoke $row containsValue $text]} {
            invoke [property get $lst nativeObject] smoothScrollToPosition $i
            break
        }
    }
}

根据API文档,getItem成员返回一个java.lang.Object,但实际上它是一个java.util.Map,因此我们可以直接调用它的containsValue方法。

这是一种最简单的方法,但仅适用于列表数据源设置为这种SimpleAdapter类型时。对于其他类型,当然可以编写修改版本的代码。这里使用scrollListToTextmain函数。

import names

def main():
    startApplication("com.froglogic.addressbook")
    openMenu(waitForObject(names.address_Book_Activity))
    tapMenuItem(waitForObject(names.address_Book_Activity), "Demo Data")
    list = waitForObject(names.address_Book_List)

    scrollListToText3(list, "Nataniel")
    tapObject(waitForObject(names.nataniel_Text))

    openMenu(waitForObject(names.edit_Address_Forename_Edit))
    tapMenuItem(waitForObject(names.edit_Address_Activity), "Cancel")
    openMenu(waitForObject(names.address_Book_List))
    tapMenuItem(waitForObject(names.address_Book_Activity), "Quit")
import * as names from 'names.js';

function main() {
    startApplication("com.froglogic.addressbook");
    openMenu(waitForObject(names.addressBookActivity));
    tapMenuItem(waitForObject(names.addressBookActivity), "Demo Data");
    list = waitForObject(names.addressBookList);

    scrollListToText2(list, "Nataniel");
    tapObject(waitForObject(names.natanielText));

    openMenu(waitForObject(names.editAddressForenameEdit));
    tapMenuItem(waitForObject(names.editAddressActivity), "Cancel");
    openMenu(waitForObject(names.addressBookList));
    tapMenuItem(waitForObject(names.addressBookActivity), "Quit");
}
require 'names.pl';

sub scrollListToText1 {
    my ($list, $text) = @_;
    my $adapter = $list->nativeObject->adapter;
    my $total = $adapter->getCount();
    for (my $i = 0; $i < $total; ++$i) {
        my $row = $adapter->getItem($i);
        if ($row->containsValue($text)) {
            $list->nativeObject->smoothScrollToPosition($i);
            last;
        }
    }
}
require 'squish'
require 'names';
include Squish

def main()
    startApplication("com.froglogic.addressbook")
    openMenu(waitForObject(Names::Address_Book_Activity))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Demo Data")
    list = waitForObject(Names::Address_Book_List)

    scrollListToText2(list, "Nataniel")
    tapObject(waitForObject(Names::Nataniel_Text))

    openMenu(waitForObject(Names::Edit_Address_Forename_Edit))
    tapMenuItem(waitForObject(Names::Edit_Address_Activity), "Cancel")
    openMenu(waitForObject(Names::Address_Book_List))
    tapMenuItem(waitForObject(Names::Address_Book_Activity), "Quit")
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "com.froglogic.addressbook"
    invoke openMenu [waitForObject $names::Address_Book_Activity]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Demo Data"
    set lst [waitForObject $names::Address_Book_List]

    scrollListToText3 $lst "Nataniel"
    invoke tapObject [waitForObject $names::Nataniel_Text]

    invoke openMenu [waitForObject $names::Edit_Address_Forename_Edit]
    invoke tapMenuItem [waitForObject $names::Edit_Address_Activity] "Cancel"
    invoke openMenu [waitForObject $names::Address_Book_List]
    invoke tapMenuItem [waitForObject $names::Address_Book_Activity] "Quit"
}
使用列表内部的对象层次结构

接下来,我们尝试使用Squish的SequenceOfObjects object.children(object)函数来遍历Squish的对象层次结构进行滚动。正如我们在object tree中看到的那样,列表中的每一行都有一个包含两个Text对象的Panel对象。只有可见的行才在这个层次结构中。因此,这个列表在向下滚动时会变化。有了这个知识,我们可以尝试按照以下方式滚动到一个项目

def scrollListToText2(list, text):
    total = list.rowCount
    current = 0
    while current < total:
        list.nativeObject.smoothScrollToPosition(current)
        rows = object.children(list)
        for row in rows:
            for textview in object.children(row):
                if textview.text == text:
                    return
        current += len(rows)
function scrollListToText2(list, text) {
    var total = list.rowCount;
    var current = 0;
    while (current < total) {
        list.nativeObject.smoothScrollToPosition(current);
        var rows = object.children(list);
        for (var r = 0; r < rows.length; ++r) {
            var columns = object.children(rows[r]);
            for (var c = 0; c < columns.length; ++c) {
                if (columns[c].text == text)
                    return;
            }
        }
        current += rows.length;
    }
    list.nativeObject.smoothScrollToPosition(total-1);
}
sub scrollListToText2 {
    my ($list, $text) = @_;
    my $total = $list->rowCount;
    my $current = 0;
    while ($current < $total) {
        $list->nativeObject->smoothScrollToPosition($current);
        my @rows = object::children($list);
        for my $row (@rows) {
            for my $textview (object::children($row)) {
                if ($textview->text eq $text) {
                    return;
                }
            }
        }
        $current += $#rows;
    }
    $list->nativeObject->smoothScrollToPosition($total-1);
}
def scrollListToText2(list, text)
    total = list.rowCount
    current = 0
    while current < total
        list.nativeObject.smoothScrollToPosition(current)
        rows = Squish::Object.children(list)
        rows.each do |row|
            for textview in Squish::Object.children(row)
                if textview.text == text
                    return
                end
            end
        end
        current += rows.length
    end
    list.nativeObject.smoothScrollToPosition(total-1)
end
proc scrollListToText2 {lst text} {
    set total [property get $lst rowCount]
    set current 0
    while {$current < $total} {
        invoke [property get $lst nativeObject] smoothScrollToPosition $current
        set rows [object children $lst]
        foreach row $rows {
            set columns [object children $row]
            foreach textview $columns {
                set tmp [property get $textview text]
                if {$tmp == $text} {
                    return
                }
            }
        }
        incr current [llength $rows]
    }
    invoke [property get $lst nativeObject] smoothScrollToPosition [expr $total - 1]
}

使用这种方法滚动到一个特定的Text对象非常依赖于精确的层次结构布局,如果在其中添加或删除了一层,则可能会出错。

就像在使用列表内部的对象层次结构中一样,我们让对象层次结构随着滚动而变化,但这次我们只是使用Boolean object.exists(objectName)函数搜索一个对象名称。

def scrollListToText3(list, text):
    objectname = {"container": names.address_Book_List, "text": text, "type": "Text", "visible": True}
    total = list.rowCount
    current = 0
    page = list.nativeObject.getLastVisiblePosition() - list.nativeObject.getFirstVisiblePosition()
    while current < total:
        list.nativeObject.smoothScrollToPosition(current)
        if object.exists(objectname):
            return
        current += page
function scrollListToText3(list, text) {
    var objectname = {"container": names.addressBookList, "text": text, "type": "Text", "visible": true}
    var total = list.rowCount;
    var page = list.nativeObject.getLastVisiblePosition() - list.nativeObject.getFirstVisiblePosition();
    for (var current = 0; current < total; current += page) {
        list.nativeObject.smoothScrollToPosition(current);
        if (object.exists(objectname))
            return;
    }
    list.nativeObject.smoothScrollToPosition(total-1);
}
sub scrollListToText3 {
    my ($list, $text) = @_;
    my $objectname = {"container" => $Names::address_book_list, "text" => $text, "type" => "Text", "visible" => "true"};
    my $total = $list->rowCount;
    my $current = 0;
    my $page = $list->nativeObject->getLastVisiblePosition() - $list->nativeObject->getFirstVisiblePosition();
    while ($current < $total) {
        $list->nativeObject->smoothScrollToPosition($current);
        if (object::exists($objectname)) {
            return;
        }
        $current += $page;
    }
    $list->nativeObject->smoothScrollToPosition($total-1);
}
def scrollListToText3(list, text)
    objectname = {:container => Names::Address_Book_List, :text => text, :type => "Text", :visible => true}
    total = list.rowCount
    current = 0
    page = list.nativeObject.getLastVisiblePosition() - list.nativeObject.getFirstVisiblePosition()
    while current < total
        list.nativeObject.smoothScrollToPosition(current)
        if Squish::Object.exists(objectname)
            return
        end
        current += page
    end
    list.nativeObject.smoothScrollToPosition(total-1)
end
proc scrollListToText3 {lst text} {
    set objectname [::Squish::ObjectName container $names::Address_Book_List text $text type Text visible true]
    set total [property get $lst rowCount]
    set current 0
    set page [expr [invoke [property get $lst nativeObject] getLastVisiblePosition] - [invoke [property get $lst nativeObject] getFirstVisiblePosition]]
    while {$current < $total} {
        invoke [property get $lst nativeObject] smoothScrollToPosition $current
        if {[object exists $objectname]} {
            return
        }
        incr current $page
    }
    invoke [property get $lst nativeObject] smoothScrollToPosition [expr $total - 1]
}

这种滚动到特定Text对象的方法是三种中最为稳健的。当应用程序的后续版本中对象层次结构发生变化时,它很可能仍然有效,因为对象名称只要求一个在List对象中的Text对象。

了解更多

现在我们已经完成了教程。Squish 能做的远不止我们在示例中展示的那么多,但目标是最快、最容易地让您开始进行基本测试。《如何创建测试脚本》(How to Create Test Scripts)和《如何测试应用程序 - 特定细节》(How to Test Applications - Specifics)部分提供了更多示例,包括如何使测试与特定输入元素(如选择框、单选框、文本和文本域)交互的示例。

《API参考手册》(API Reference)和《工具参考手册》(Tools Reference)提供了Squish测试API的全面详细信息以及它提供的多功能性,使其测试尽可能简单和高效。阅读《如何创建测试脚本》(How to Create Test Scripts)、《如何测试应用程序 - 特定细节》(How to Test Applications - Specifics),以及浏览《API参考手册》(API Reference)和《工具参考手册》(Tools Reference)是非常值得的。您投入的时间将会得到回报,因为您将了解Squish提供了哪些开箱即用的功能,并可以避免重复发明已经存在的东西。

Squish for Android 支持手势的录制和回放,同时还提供了一个强大的API在脚本中创建或操作它们。更多阅读,请参阅如何使用GestureBuilder类

以下给出了关键的Android示例以及它们的使用位置。

  • 地址簿展示了如何测试一些Android小部件,例如:ButtonTextEditListViewMenu
  • WebBrowserHost展示了如何使用内嵌的Android WebView小部件进行测试。

教程:设计行为驱动开发(BDD)测试

本教程将向您展示如何为示例应用程序创建、运行和修改行为驱动开发(BDD)测试。您将了解Squish最常用的功能。在教程结束时,您将能够为您自己的应用程序编写自己的测试。

在本章中,我们将使用一个简单的地址簿应用程序作为我们的应用程序(AUT)。这是一个非常基础的应用程序,允许用户添加、编辑和删除条目。截图显示了用户正在添加新条目的应用程序操作。

"The Android addressbook example"

行为驱动开发(BDD)简介

行为驱动开发(BDD)是测试驱动开发(TDD)方法的扩展,它将验收标准的定义置于开发过程的开头,而不是在软件开发之后编写测试。可能的代码更改循环在测试之后进行。

"BDD process"

行为驱动测试由一组Feature文件组成,通过一个或多个Scenario中预期应用行为来描述产品功能。每个Scenario由一系列步骤组成,这些步骤代表了需要对该Scenario进行测试的操作或验证。

BDD侧重于预期应用行为,而不是实现细节。因此,BDD测试以可读的领域特定语言(DSL)描述。由于这种语言不是技术性的,因此除了程序员外,产品负责人、测试人员或商务分析师也可以创建这样测试。此外,在产品开发期间,此类测试还作为产品文档的使用。对于Squish使用,BDD测试应使用Gherkin语法创建。之前编写的产物规范(BDD测试)可以转换为可执行测试。这个分步教程展示了使用squishide支持自动化BDD测试。

Gherkin语法

Gerkin 文件通过一个或多个场景中预期的应用程序行为来描述产品功能。以下是一个展示地址簿示例应用程序的“地址簿填充”功能的示例。

Feature: Filling of addressbook
    As a user I want to fill the addressbook with entries

    Scenario: Initial state of created address book
        Given addressbook application is running
        Then addressbook should have zero entries

    Scenario: State after adding one entry
        Given addressbook application is running
        When I add a new person 'John','Doe','[email protected]','500600700' to address book
        Then '1' entries should be present

    Scenario: State after adding two entries
        Given addressbook application is running
        When I add new persons to address book
            | forename  | surname  | email        | phone  |
            | John      | Smith    | john@m.com   | 123123 |
            | Alice     | Thomson  | alice@m.com  | 234234 |
        Then '2' entries should be present

    Scenario: Forename and surname is added to table
        Given addressbook application is running
        When I add a new person 'Bob','Doe','[email protected]','123321231' to address book
        Then previously entered forename and surname shall be at the top

上述大部分是自由文本(不一定要是英文)。只是 功能 / 场景 结构以及“Given”、“And”、“When”和 “Then” 等前置关键字是固定的。这些关键字中的每一个都标记了一个定义前置条件、用户操作和预期结果步骤。上述应用程序行为描述可以传递给软件开发者来实现这些功能,同时也可以将相同的描述传递给软件测试者来实现自动化测试。

测试实现

创建测试套件

首先,我们需要创建一个测试套件,它是一个所有测试用例的容器。启动 squishide,选择 文件 > 新建测试套件 。请按照新建测试套件向导操作,提供测试套件名称,选择您选择的 Android 工具包和脚本语言,最后将地址簿应用注册为 AUT。有关创建新测试套件的更多信息,请参考 创建测试套件

创建测试用例

Squish 提供两种测试用例类型:脚本测试用例和BDD测试用例。由于“脚本测试用例”是默认的,因此为了创建新的“BDD测试用例”,我们需要通过点击 新建脚本测试用例) 旁边的展开器并选择 新建BDD测试用例 来使用上下文菜单。 Squishide 会记住您的选择,并且在未来点击按钮时,BDD测试用例将成为默认设置。

"Creating new BDD Test Case"

新创建的BDD测试用例包括一个 test.feature 文件(在创建新的BDD测试用例时填充了Gerkin模板),一个名为 test.(py|js|pl|rb|tcl) 的文件,该文件将驱动执行(无需编辑此文件),以及在测试套件资源中名为 steps/steps.(py|js|pl|rb|tcl) 的文件,其中将放置步骤实现代码。

我们需要用地址簿示例应用程序的 功能 替换 Gerkin 模板。为此,复制下面的 功能 描述并将其粘贴到 功能 文件中。

Feature: Filling of addressbook
    As a user I want to fill the addressbook with entries

    Scenario: Initial state of created address book
        Given addressbook application is running
        Then addressbook should have zero entries

在编辑 test.feature 文件时,每个未定义步骤都会显示一个 功能 文件警告 未找到实现。这些实现位于 steps 子目录中,在 测试用例资源 中,或在 测试套件资源 中。现在运行我们的 功能 测试现在将在第一步失败,显示没有匹配的步骤定义,并且后续步骤将被跳过。

记录步骤实现

为了记录 场景,请按 记录按钮旁边的相应 场景,该 场景 位于 测试用例资源 视图中 场景 选项卡中。

"Record Scenario"

这将导致Squish运行AUT,以便我们可以与它交互。此外,控制栏会显示所有需要记录的步骤列表。现在,AUT或任何添加到脚本中的验证点与第一个步骤“Given地址簿应用正在运行”(在控制栏的步骤列表中被加粗)的所有交互将被记录。为了验证这个先决条件,我们将添加一个验证点。为此,点击控制栏中的验证,然后选择属性

"Control Bar"

因此,squishide被置于Spy视角,显示应用程序对象属性的视图。在应用程序对象树中,选择AddressBook Activity。选择它将更新右侧的属性视图。接下来,点击属性视图中的启用复选框。最后,点击保存并插入验证按钮。squishide消失,再次显示控制栏。

"Inserting Verification Point"

当我们完成每个步骤时,我们可以通过单击控制栏左侧当前步骤左侧的完成录制步骤)箭头按钮来移动到下一个未定义的步骤(回放先前定义的步骤)。

接下来,对于步骤Then addressbook should have zero entries,验证包含地址条目的表是否为空。要记录此验证,在录制时点击验证,选择属性。在应用程序对象视图中,使用对象选择器)工具选择包含地址簿条目的Table对象(在我们的例子中,此表为空)。从属性视图中检查rowCount属性,点击保存并插入验证。最后,点击控制栏中的最后一个完成录制步骤)箭头按钮。

因此,Squish将在steps.*文件中生成以下步骤定义(在测试套件 > 测试套件资源

@Given("addressbook application is running")
def step(context):
    startApplication("com.froglogic.addressbook")
    test.compare(waitForObjectExists(names.address_Book_Activity).enabled, True)

@Then("addressbook should have zero entries")
def step(context):
    test.compare(waitForObjectExists(names.address_Book_addressList_List).rowCount, 0)
Given("addressbook application is running", function(context) {
    startApplication("com.froglogic.addressbook");
    test.compare(waitForObjectExists(names.addressBookActivity).enabled, true);
});

Then("addressbook should have zero entries", function(context) {
    test.compare(waitForObjectExists(names.addressBookAddressListList).rowCount, 0);
});
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("com.froglogic.addressbook");
    test::compare(waitForObjectExists($Names::address_book_activity)->enabled, 1);
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::address_book_addresslist_list)->rowCount, 0);
});
Given("addressbook application is running") do |context|
    startApplication("com.froglogic.addressbook")
    Test.compare(waitForObjectExists(Names::Address_Book_Activity).enabled, true)
end

Then("addressbook should have zero entries") do |context|
    Test.compare(waitForObjectExists(Names::Address_Book_addressList_List).rowCount, 0)
end
Given "addressbook application is running" {context} {
    startApplication "com.froglogic.addressbook"
    test compare [property get [waitForObjectExists $names::Address_Book_Activity] enabled] true
}

Then "addressbook should have zero entries" {context} {
    test compare [property get [waitForObjectExists $names::Address_Book_addressList_List] rowCount] 0
}

应用程序由于记录的startApplication()调用,在第一个步骤的开始自动启动。在每个场景的末尾,将调用OnScenarioEnd钩子,导致在应用程序上下文中调用detach()。因为AUT是用startApplication()启动的,这会使它终止。此钩子函数位于脚本标签中的文件bdd_hooks.(py|js|pl|rb|tcl)中,该文件位于测试套件资源视图的脚本标签中。您可以在其中定义额外的钩子函数。对于所有可用的钩子函数的列表,请参阅通过钩子在测试执行期间执行操作

@OnScenarioEnd
def OnScenarioEnd():
    for ctx in applicationContextList():
        ctx.detach()
OnScenarioEnd(function(context) {
    applicationContextList().forEach(function(ctx) { ctx.detach(); });
});
OnScenarioEnd(sub {
    foreach (applicationContextList()) {
        $_->detach();
    }
});
OnScenarioEnd do |context|
    applicationContextList().each { |ctx| ctx.detach() }
end
OnScenarioEnd {context} {
    foreach ctx [applicationContextList] {
        applicationContext $ctx detach
    }
}

步骤参数化

迄今为止,我们的步骤没有使用任何参数,所有值都是硬编码的。Squish有不同类型的参数,如anyintegerword,使我们的步骤定义更具可复用性。让我们向我们的Feature文件添加一个新的Scenario,其中将为测试数据和预期结果提供步骤参数。将下面的部分复制到您的Feature文件中。

Scenario: State after adding one entry
    Given addressbook application is running
    When I add a new person 'John','Doe','[email protected]','500600700' to address book
    Then '1' entries should be present

在自动保存Feature文件后,squishide提供了一个提示,说明只需要实现两个步骤:When I add a new person 'John', 'Doe','[email protected]','500600700' to address bookThen '1' entries should be present。其余步骤已具有匹配的步骤实现。

要记录缺失的步骤,请在测试套件视图中测试用例名称旁边的“记录”按钮上点击。脚本将播放到缺失的步骤并提示您实现它。如果您选择“添加”按钮,则可以输入新条目的信息。点击完成记录步骤()按钮以便跳转到下一个步骤。对于第二个缺失步骤,我们可以记录一个对象属性验证,就像我们对步骤Then addressbook should have zero entries所做的那样。或者,我们可以将步骤的实现复制到steps.(py|js|pl|rb|tcl)文件中,并增加test.compare行末的数字。我们不再是测试零个物品,而是在测试一个物品。

现在,通过用参数类型替换值,我们对生成的When步骤实现进行参数化。由于我们想能够添加不同的名称,将‘John’替换为‘|word|’。每个参数将按步骤描述名中出现的顺序传递给步骤实现函数。通过编辑输入的值到关键字来完成参数化,使其看起来像这个示例步骤When I add a new person 'John', 'Doe','[email protected]','500600700' to address book

@When("I add a new person '|word|','|word|','|any|','|integer|' to address book")
def step(context, forename, surname, email, phone):
    tapObject(waitForObject(names.address_Book_Add_Address_Button))
    tapObject(waitForObject(names.edit_Address_Forename_Edit))
    type(waitForObject(names.edit_Address_Forename_Edit), forename)
    tapObject(waitForObject(names.edit_Address_Surname_Edit))
    type(waitForObject(names.edit_Address_Surname_Edit), surname)
    tapObject(waitForObject(names.edit_Address_Phone_Edit))
    type(waitForObject(names.edit_Address_Phone_Edit), phone)
    tapObject(waitForObject(names.edit_Address_Email_Edit))
    type(waitForObject(names.edit_Address_Email_Edit), email)
    tapObject(waitForObject(names.edit_Address_Save_Button))
    context.userData = {}
    context.userData['forename'] = forename
    context.userData['surname'] = surname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) {
    tapObject(waitForObject(names.addressBookAddAddressButton));
    tapObject(waitForObject(names.editAddressForenameEdit));
    type(waitForObject(names.editAddressForenameEdit), forename);
    tapObject(waitForObject(names.editAddressSurnameEdit));
    type(waitForObject(names.editAddressSurnameEdit), surname);
    tapObject(waitForObject(names.editAddressPhoneEdit));
    type(waitForObject(names.editAddressPhoneEdit), phone);
    tapObject(waitForObject(names.editAddressEmailEdit));
    type(waitForObject(names.editAddressEmailEdit), email);
    tapObject(waitForObject(names.editAddressSaveButton));
    context.userData = {};
    context.userData['forename'] = forename;
    context.userData['surname'] = surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub {
    my ($context, $forename, $surname, $email, $phone) = @_;
    tapObject(waitForObject($Names::address_book_add_address_button));
    tapObject(waitForObject($Names::edit_address_forename_edit));
    type(waitForObject($Names::edit_address_forename_edit), $forename);
    tapObject(waitForObject($Names::edit_address_surname_edit));
    type(waitForObject($Names::edit_address_surname_edit), $surname);
    tapObject(waitForObject($Names::edit_address_phone_edit));
    type(waitForObject($Names::edit_address_phone_edit), $phone);
    tapObject(waitForObject($Names::edit_address_email_edit));
    type(waitForObject($Names::edit_address_email_edit), $email);
    tapObject(waitForObject($Names::edit_address_save_button));
    $context->{userData}{'forename'} = $forename;
    $context->{userData}{'surname'} = $surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone|
    tapObject(waitForObject(Names::Address_Book_Add_Address_Button))
    tapObject(waitForObject(Names::Edit_Address_Forename_Edit))
    type(waitForObject(Names::Edit_Address_Forename_Edit), forename)
    tapObject(waitForObject(Names::Edit_Address_Surname_Edit))
    type(waitForObject(Names::Edit_Address_Surname_Edit), surname)
    tapObject(waitForObject(Names::Edit_Address_Phone_Edit))
    type(waitForObject(Names::Edit_Address_Phone_Edit), phone)
    tapObject(waitForObject(Names::Edit_Address_Email_Edit))
    type(waitForObject(Names::Edit_Address_Email_Edit), email)
    tapObject(waitForObject(Names::Edit_Address_Save_Button))
    context.userData = Hash.new
    context.userData[:forename] = forename
    context.userData[:surname] = surname
end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} {
    invoke tapObject [waitForObject $names::Address_Book_Add_Address_Button]
    invoke tapObject [waitForObject $names::Edit_Address_Forename_Edit]
    invoke type [waitForObject $names::Edit_Address_Forename_Edit] $forename
    invoke tapObject [waitForObject $names::Edit_Address_Surname_Edit]
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] $surname
    invoke tapObject [waitForObject $names::Edit_Address_Phone_Edit]
    invoke type [waitForObject $names::Edit_Address_Phone_Edit] $phone
    invoke tapObject [waitForObject $names::Edit_Address_Email_Edit]
    invoke type [waitForObject $names::Edit_Address_Email_Edit] $email
    invoke tapObject [waitForObject $names::Edit_Address_Save_Button]
    $context userData [dict create forename $forename surname $surname]
}

如果我们记录了最后的Then作为一个缺失的步骤,并且验证表格中的rowCount是1,我们可以修改该步骤,使其接受一个参数,以便以后验证其他整数值。

@Then("'|integer|' entries should be present")
def step(context, count):
    test.compare(waitForObjectExists(names.address_Book_addressList_List).rowCount, count)
Then("'|integer|' entries should be present", function(context, count) {
    test.compare(waitForObjectExists(names.addressBookAddressListList).rowCount, count);
});
Then("'|integer|' entries should be present", sub {
    my ($context, $count) = @_;
    test::compare(waitForObjectExists($Names::address_book_addresslist_list)->rowCount, $count);
});
Then("'|integer|' entries should be present") do |context, count|
    Test.compare(waitForObjectExists(Names::Address_Book_addressList_List).rowCount, count)
end
Then "'|integer|' entries should be present" {context count} {
    test compare [property get [waitForObjectExists $names::Address_Book_addressList_List] rowCount] $count
}

在表格中为步骤提供参数

下一个Scenario将测试向地址簿添加多个条目。我们可以使用步骤When I add a new person John','Doe','[email protected]','500600700' to address book多次只是用不同的数据。但让我们定义一个新的步骤,称为When I add a new person to address book,它将从表中处理数据。

When I add new persons to address book
    | forename  | surname  | email        | phone  |
    | John      | Smith    | john@m.com   | 123123 |
    | Alice     | Thomson  | alice@m.com  | 234234 |

处理此类表格的步骤实现看起来像这样

@When("I add new persons to address book")
def step(context):
    table = context.table
    # Drop initial row with column headers
    table.pop(0)
    for (forename, surname, email, phone) in table:
        tapObject(waitForObject(names.address_Book_Add_Address_Button))
        tapObject(waitForObject(names.edit_Address_Forename_Edit))
        type(waitForObject(names.edit_Address_Forename_Edit), forename)
        tapObject(waitForObject(names.edit_Address_Surname_Edit))
        type(waitForObject(names.edit_Address_Surname_Edit), surname)
        tapObject(waitForObject(names.edit_Address_Phone_Edit))
        type(waitForObject(names.edit_Address_Phone_Edit), phone)
        tapObject(waitForObject(names.edit_Address_Email_Edit))
        type(waitForObject(names.edit_Address_Email_Edit), email)
        tapObject(waitForObject(names.edit_Address_Save_Button))
When("I add new persons to address book", function(context) {
    var table = context.table;

    // Skip initial row with column headers by starting at index 1
    for (var i = 1; i < table.length; ++i) {
        var forename = table[i][0];
        var surname = table[i][1];
        var email = table[i][2];
        var phone = table[i][3];
        tapObject(waitForObject(names.addressBookAddAddressButton));
        tapObject(waitForObject(names.editAddressForenameEdit));
        type(waitForObject(names.editAddressForenameEdit), forename);
        tapObject(waitForObject(names.editAddressSurnameEdit));
        type(waitForObject(names.editAddressSurnameEdit), surname);
        tapObject(waitForObject(names.editAddressPhoneEdit));
        type(waitForObject(names.editAddressPhoneEdit), phone);
        tapObject(waitForObject(names.editAddressEmailEdit));
        type(waitForObject(names.editAddressEmailEdit), email);
        tapObject(waitForObject(names.editAddressSaveButton));
    }
});
When("I add new persons to address book", sub {
    my $context = shift;
    my $table = $context->{'table'};
    # Drop initial row with column headers
    shift(@{$table});
    for my $row (@{$table}) {
        my ($forename, $surname, $email, $phone) = @{$row};
        tapObject(waitForObject($Names::address_book_add_address_button));
        tapObject(waitForObject($Names::edit_address_forename_edit));
        type(waitForObject($Names::edit_address_forename_edit), $forename);
        tapObject(waitForObject($Names::edit_address_surname_edit));
        type(waitForObject($Names::edit_address_surname_edit), $surname);
        tapObject(waitForObject($Names::edit_address_phone_edit));
        type(waitForObject($Names::edit_address_phone_edit), $phone);
        tapObject(waitForObject($Names::edit_address_email_edit));
        type(waitForObject($Names::edit_address_email_edit), $email);
        tapObject(waitForObject($Names::edit_address_save_button));
    }
});
When("I add new persons to address book") do |context|
    table = context.table
    # Drop initial row with column headers
    table.shift
    for forename, surname, email, phone in table do
        tapObject(waitForObject(Names::Address_Book_Add_Address_Button))
        tapObject(waitForObject(Names::Edit_Address_Forename_Edit))
        type(waitForObject(Names::Edit_Address_Forename_Edit), forename)
        tapObject(waitForObject(Names::Edit_Address_Surname_Edit))
        type(waitForObject(Names::Edit_Address_Surname_Edit), surname)
        tapObject(waitForObject(Names::Edit_Address_Phone_Edit))
        type(waitForObject(Names::Edit_Address_Phone_Edit), phone)
        tapObject(waitForObject(Names::Edit_Address_Email_Edit))
        type(waitForObject(Names::Edit_Address_Email_Edit), email)
        tapObject(waitForObject(Names::Edit_Address_Save_Button))
    end
end
When "I add new persons to address book" {context} {
    set table [$context table]
    # Drop initial row with column headers
    foreach row [lreplace $table 0 0] {
        foreach {forename surname email phone} $row break
        invoke tapObject [waitForObject $names::Address_Book_Add_Address_Button]
        invoke tapObject [waitForObject $names::Edit_Address_Forename_Edit]
        invoke type [waitForObject $names::Edit_Address_Forename_Edit] $forename
        invoke tapObject [waitForObject $names::Edit_Address_Surname_Edit]
        invoke type [waitForObject $names::Edit_Address_Surname_Edit] $surname
        invoke tapObject [waitForObject $names::Edit_Address_Phone_Edit]
        invoke type [waitForObject $names::Edit_Address_Phone_Edit] $phone
        invoke tapObject [waitForObject $names::Edit_Address_Email_Edit]
        invoke type [waitForObject $names::Edit_Address_Email_Edit] $email
        invoke tapObject [waitForObject $names::Edit_Address_Save_Button]
    }
}

在步骤和场景之间共享数据

让我们在Feature文件中添加一个新的Scenario。这次我们想检查地址簿列表中的条目数,而不是列表是否包含正确的数据。因为我们在一个步骤中输入数据并在另一个步骤中进行验证,我们必须在那些步骤之间共享有关输入数据的信息,以便执行验证。

Scenario: Forename and surname is added to table
    Given addressbook application is running
    When I add a new person 'Bob','Doe','[email protected]','123321231' to address book
    Then previously entered forename and surname shall be at the top

要共享此数据,可以使用context.userData。

@When("I add a new person '|word|','|word|','|any|','|integer|' to address book")
def step(context, forename, surname, email, phone):
    tapObject(waitForObject(names.address_Book_Add_Address_Button))
    tapObject(waitForObject(names.edit_Address_Forename_Edit))
    type(waitForObject(names.edit_Address_Forename_Edit), forename)
    tapObject(waitForObject(names.edit_Address_Surname_Edit))
    type(waitForObject(names.edit_Address_Surname_Edit), surname)
    tapObject(waitForObject(names.edit_Address_Phone_Edit))
    type(waitForObject(names.edit_Address_Phone_Edit), phone)
    tapObject(waitForObject(names.edit_Address_Email_Edit))
    type(waitForObject(names.edit_Address_Email_Edit), email)
    tapObject(waitForObject(names.edit_Address_Save_Button))
    context.userData = {}
    context.userData['forename'] = forename
    context.userData['surname'] = surname
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", function(context, forename, surname, email, phone) {
    tapObject(waitForObject(names.addressBookAddAddressButton));
    tapObject(waitForObject(names.editAddressForenameEdit));
    type(waitForObject(names.editAddressForenameEdit), forename);
    tapObject(waitForObject(names.editAddressSurnameEdit));
    type(waitForObject(names.editAddressSurnameEdit), surname);
    tapObject(waitForObject(names.editAddressPhoneEdit));
    type(waitForObject(names.editAddressPhoneEdit), phone);
    tapObject(waitForObject(names.editAddressEmailEdit));
    type(waitForObject(names.editAddressEmailEdit), email);
    tapObject(waitForObject(names.editAddressSaveButton));
    context.userData = {};
    context.userData['forename'] = forename;
    context.userData['surname'] = surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book", sub {
    my ($context, $forename, $surname, $email, $phone) = @_;
    tapObject(waitForObject($Names::address_book_add_address_button));
    tapObject(waitForObject($Names::edit_address_forename_edit));
    type(waitForObject($Names::edit_address_forename_edit), $forename);
    tapObject(waitForObject($Names::edit_address_surname_edit));
    type(waitForObject($Names::edit_address_surname_edit), $surname);
    tapObject(waitForObject($Names::edit_address_phone_edit));
    type(waitForObject($Names::edit_address_phone_edit), $phone);
    tapObject(waitForObject($Names::edit_address_email_edit));
    type(waitForObject($Names::edit_address_email_edit), $email);
    tapObject(waitForObject($Names::edit_address_save_button));
    $context->{userData}{'forename'} = $forename;
    $context->{userData}{'surname'} = $surname;
});
When("I add a new person '|word|','|word|','|any|','|integer|' to address book") do |context, forename, surname, email, phone|
    tapObject(waitForObject(Names::Address_Book_Add_Address_Button))
    tapObject(waitForObject(Names::Edit_Address_Forename_Edit))
    type(waitForObject(Names::Edit_Address_Forename_Edit), forename)
    tapObject(waitForObject(Names::Edit_Address_Surname_Edit))
    type(waitForObject(Names::Edit_Address_Surname_Edit), surname)
    tapObject(waitForObject(Names::Edit_Address_Phone_Edit))
    type(waitForObject(Names::Edit_Address_Phone_Edit), phone)
    tapObject(waitForObject(Names::Edit_Address_Email_Edit))
    type(waitForObject(Names::Edit_Address_Email_Edit), email)
    tapObject(waitForObject(Names::Edit_Address_Save_Button))
    context.userData = Hash.new
    context.userData[:forename] = forename
    context.userData[:surname] = surname
end
When "I add a new person '|word|','|word|','|any|','|integer|' to address book" {context forename surname email phone} {
    invoke tapObject [waitForObject $names::Address_Book_Add_Address_Button]
    invoke tapObject [waitForObject $names::Edit_Address_Forename_Edit]
    invoke type [waitForObject $names::Edit_Address_Forename_Edit] $forename
    invoke tapObject [waitForObject $names::Edit_Address_Surname_Edit]
    invoke type [waitForObject $names::Edit_Address_Surname_Edit] $surname
    invoke tapObject [waitForObject $names::Edit_Address_Phone_Edit]
    invoke type [waitForObject $names::Edit_Address_Phone_Edit] $phone
    invoke tapObject [waitForObject $names::Edit_Address_Email_Edit]
    invoke type [waitForObject $names::Edit_Address_Email_Edit] $email
    invoke tapObject [waitForObject $names::Edit_Address_Save_Button]
    $context userData [dict create forename $forename surname $surname]
}

可以访问在给定Feature的所有Scenarios的所有步骤和Hooks中存储在context.userData中的所有数据。最后,我们需要实现步骤Then previously entered forename and surname shall be at the top

@Then("previously entered forename and surname shall be at the top")
def step(context):
    list = waitForObject(names.address_Book_addressList_List)
    rows = object.children(list)
    row_0 = rows[0]
    columns_row_0 = object.children(row_0)
    test.compare(columns_row_0[0].text, context.userData["forename"])
    test.compare(columns_row_0[1].text, context.userData["surname"])
Then("previously entered forename and surname shall be at the top", function(context) {
    var list = waitForObject(names.addressBookAddressListList);
    var rows = object.children(list);
    var row_0 = rows[0];
    var columns_row_0 = object.children(row_0);
    test.compare(columns_row_0[0].text, context.userData["forename"]);
    test.compare(columns_row_0[1].text, context.userData["surname"]);
});
Then("previously entered forename and surname shall be at the top", sub {
    my $context = shift;
    my $list = waitForObject($Names::address_book_addresslist_list);
    my @rows = object::children($list);
    my $row_0 = $rows[0];
    my @columns_row_0 = object::children($row_0);
    test::compare($columns_row_0[0]->text, $context->{userData}{'forename'}, "forename?");
    test::compare($columns_row_0[1]->text, $context->{userData}{'surname'}, "surname?");
});
Then("previously entered forename and surname shall be at the top") do |context|
    list = waitForObject(Names::Address_Book_addressList_List)
    rows = Squish::Object.children(list)
    row_0 = rows[0]
    columns_row_0 = Squish::Object.children(row_0)
    Test.compare(columns_row_0[0].text, context.userData[:forename])
    Test.compare(columns_row_0[1].text, context.userData[:surname])
end
Then "previously entered forename and surname shall be at the top" {context} {
    set list [waitForObject $names::Address_Book_addressList_List]
    set rows [object children $list]
    set row_0 [lindex $rows 0]
    set columns_row_0 [object children $row_0]
    test compare [property get [lindex $columns_row_0 0] text] [dict get [$context userData] "forename"]
    test compare [property get [lindex $columns_row_0 1] text] [dict get [$context userData] "surname"]
}

场景大纲

假设我们的Feature包含以下两个Scenarios

Scenario: State after adding one entry
    Given addressbook application is running
    When I add a new person 'John','Doe','[email protected]','500600700' to address book
    Then "1" entries should be present

Scenario: State after adding one entry
    Given addressbook application is running
    When I add a new person 'Bob','Koo','[email protected]','500600800' to address book
    Then "1" entries should be present

正如我们所看到的,这些Scenarios使用不同的测试数据执行相同的行为。这可以通过使用Scenario Outline(带有占位符的Scenario模板)和示例(带有参数的表)来实现。

Scenario Outline: Adding single entries multiple time
    Given addressbook application is running
    When I add a new person '<forename>','<surname>','<email>','<phone>' to address book
    Then '1' entries should be present
    Examples:
        | forename | surname  | email       | phone     |
        | John     | Doe      | john@m.com  | 500600700 |
        | Bob      | Koo      | bob@m.com   | 500600800 |

在每个循环迭代的末尾将执行OnScenarioEnd钩子。

测试执行

squishide 中,用户可以执行一个 Feature 中的一个所有 Scenario,或者只执行一个选定的 Scenario。要执行所有 Scenario,需要点击测试套件视图中的 播放 按钮来执行合适的测试用例。

"Execute all Scenarios from Feature"

要执行单个 Scenario,您需要打开 Feature 文件,在指定的 Scenario 上右键点击,并选择 运行 Scenario。另一个方法是点击 播放 按钮旁边的 Scenario,在 测试用例资源Scenario 选项卡中。

"Execute one Scenario from Feature"

一个 Scenario 执行完成后,根据执行结果,Feature 文件将着色。更详细的信息(如日志)可以在测试结果视图中找到。

"Execution results in Feature file"

测试调试

Squish 提供了在任何地方暂停测试用例执行的功能,以便检查脚本变量、监视应用程序对象或运行在 Squish 脚本控制台 中的自定义代码。为此,需要在开始执行之前放置一个断点,无论是在任何包含步骤的 Feature 文件的行中,还是在已执行代码的任何一行(即在步骤定义代码的中间)。

"Breakpoint in Feature file"

断点被到达后,您可以检查所有应用程序对象及其属性。如果断点放置在步骤定义或钩子处,还可以添加验证点或记录代码片段。

重用步骤定义

通过在另一个目录中的测试用例中重用步骤定义,可以提高 BDD 测试的可维护性。有关更多信息,请参阅 collectStepDefinitions()

教程:将现有测试迁移到 BDD

本章适用于那些拥有现有 Squish 测试并希望引入行为驱动测试的用户。第一部分描述了如何保留现有测试并简单地添加新的 BDD 测试。第二部分描述了如何将基于脚本的测试转换为 BDD 测试。

扩展现有测试到 BDD

第一种方法是保留现有 Squish 测试并添加新的 BDD 测试。可以将包含脚本和 BDD 测试用例的 Test Suite。只需打开现有的 Test Suite,然后在 新建脚本测试用例 工具栏按钮右侧的下拉菜单中选择 新建 BDD 测试用例 选项( )。

"Creating new BDD Test Case"

假设您的现有测试用例使用了库并调用了与 AUT 交互的共享函数,这些函数也可以在 BDD 测试用例中使用。以下示例中,一个函数被多个基于脚本的测试用例调用

def deleteFirstEntry():
    #...
    pass
function deleteFirstEntry(){
    //...
}
sub deleteFirstEntry{
    #...
}
def deleteFirstEntry
  #...
end
proc deleteFirstEntry {} {
    #...
}

新的 BDD 测试用例可以轻松地使用同一个函数

@When("I delete the first entry")
def step(context):
    deleteFirstEntry()
When("I delete the first entry", function(context){
    deleteFirstEntry()
});
When("I delete the first entry", sub {
    deleteFirstEntry();
});
When("I delete the first entry") do |context|
  deleteFirstEntry
end
When "I delete the first entry" {context} {
    deleteFirstEntry
}

将现有测试转换为 BDD

第二种方法是转换包含基于脚本的测试用例的现有 Test Suite 为行为驱动测试。由于 Test Suite 可以包含基于脚本和 BDD 测试用例,迁移可以逐步进行。一个包含两种测试用例类型的 Test Suite 可以无需额外工作即可执行并分析结果。

第一步是审查现有测试套件的所有用例,并按它们测试的功能进行分组。每个基于脚本的用例将转换为一个场景,它是功能的一部分。例如,假设我们有5个基于脚本的用例。分析后,我们发现它们检查了两个功能。因此,迁移完成后,我们的测试套件将包含两个BDD用例,每个用例包含一个功能。每个功能将包含多个场景。在我们的例子中,第一个功能包含三个场景,第二个功能包含两个场景

"Conversion Chart"

开始时,在包含计划迁移到BDD的Squish测试的squishide中打开一个测试套件。然后,从上下文菜单中选择新建BDD用例选项来创建一个新用例。每个BDD用例包含一个可以填充最多一个功能test.feature文件。接下来,打开test.feature文件,使用Gherkin语言描述功能。根据模板的语法,编辑功能名称,并可选地提供简短描述。接下来,分析在脚本用例中执行的需要迁移的操作和验证。以下是一个用于addressbook应用示例的BDD用例可能看起来像:

def main():
    startApplication("com.froglogic.addressbook")
    test.compare(waitForObjectExists(names.address_Book_List).rowCount, 0, "Addressbook is empty?")
function main(){
    startApplication("com.froglogic.addressbook");
    test.compare(waitForObjectExists(names.addressBookList).rowCount, 0, "Addressbook is empty?");
}
sub main {
    startApplication("com.froglogic.addressbook");
    test::compare(waitForObjectExists($Names::address_book_list)->rowCount,0, "Addressbook is empty?");
}
def main
  startApplication("com.froglogic.addressbook")
  Test.compare(waitForObjectExists(Names::Address_Book_List).rowCount, 0, "Addressbook is empty?")
end
proc main {} {
    startApplication "com.froglogic.addressbook"
    test compare [property get [waitForObjectExists $names::Address_Book_List] rowCount] 0
}

分析上述脚本测试后,我们可以创建以下场景并将其添加到test.feature文件中:

Scenario: Initial state of created address book
   Given addressbook application is running
   Then addressbook should have zero entries

接下来,在场景上右键单击,并从上下文菜单中选择创建缺失步骤实现选项。这将创建步骤定义的骨架。

@Given("addressbook application is running")
def step(context):
    test.warning("TODO implement addressbook application is running")

@Then("addressbook should have zero entries")
def step(context):
    test.warning("TODO implement addressbook should have zero entries")
Given("addressbook application is running", function(context) {
    test.warning("TODO implement addressbook application is running");
});

Then("addressbook should have zero entries", function(context) {
    test.warning("TODO implement addressbook should have zero entries");
});
Given("addressbook application is running", sub {
    my $context = shift;
    test::warning("TODO implement addressbook application is running");
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::warning("TODO implement addressbook should have zero entries");
});
Given("addressbook application is running") do |context|
  Test.warning "TODO implement addressbook application is running"
end

Then("addressbook should have zero entries") do |context|
  Test.warning "TODO implement addressbook should have zero entries"
end
Given "addressbook application is running" {context} {
    test warning "TODO implement addressbook application is running"
}

Then "addressbook should have zero entries" {context} {
    test warning "TODO implement addressbook should have zero entries"
}

现在我们将从脚本测试中提取代码片段放入相应的步骤定义中,并删除包含test.warning的行。如果您的脚本是测试使用共享脚本,您也可以在步骤定义中调用那些函数。例如,最终结果可能如下所示:

@Given("addressbook application is running")
def step(context):
    startApplication("com.froglogic.addressbook")

@Then("addressbook should have zero entries")
def step(context):
    test.compare(waitForObjectExists(names.address_Book_List).rowCount, 0,  "Addressbook is empty?")
Given("addressbook application is running", function(context) {
    startApplication("com.froglogic.addressbook");
});

Then("addressbook should have zero entries", function(context) {
        test.compare(waitForObjectExists(names.addressBookList).rowCount, 0);
});
Given("addressbook application is running", sub {
    my $context = shift;
    startApplication("com.froglogic.addressbook");
});

Then("addressbook should have zero entries", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::address_book_list)->rowCount,0);
});
Given("addressbook application is running") do |context|
  startApplication("com.froglogic.addressbook")
end

Then("addressbook should have zero entries") do |context|
  Test.compare(waitForObjectExists(Names::Address_Book_List).rowCount, 0, "Addressbook is empty?")
end
Given "addressbook application is running" {context} {
    startApplication "com.froglogic.addressbook"
}

Then "addressbook should have zero entries" {context} {
    test compare [property get [waitForObjectExists $names::Address_Book_List] rowCount] 0
}

每次运行每个场景后,都会终止AUT,如下所示的自动生成的OnScenarioEnd钩子文件所示:

@OnScenarioEnd
def hook(context):
    for ctx in applicationContextList():
        ctx.detach()
OnScenarioEnd(function(context) {
    applicationContextList().forEach(function(ctx) { ctx.detach(); });
});
OnScenarioEnd(sub {
    foreach (applicationContextList()) {
        $_->detach();
    }
});
OnScenarioEnd do |context|
    applicationContextList().each { |ctx| ctx.detach() }
end
OnScenarioEnd {context} {
    foreach ctx [applicationContextList] {
        applicationContext $ctx detach
    }
}

上述示例为了本教程而简化。要充分发挥Squish中的行为驱动测试的优势,请熟悉行为驱动测试部分,见API参考

©2024 Qt公司有限公司版权所有。本文件包含的文档贡献属于其各自的拥有者。
本文件提供的文档是根据遵循自由软件基金会发布的GNU自由文档许可证版本1.3的条款许可的。
Qt及其相关标志是芬兰Qt公司及其在世界其他地区的商标。所有其他商标均为其各自所有者的财产。