Squish for iOS教程

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

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

注意: iOS 应用程序只能在苹果硬件上测试,无论是设备本身还是在 macOS 上运行的 iOS 模拟器。

要测试 iOS 上的网络应用程序,如 Safari 应用程序,请使用 Squish for Web 版本并进行一些额外设置。安装 Squish for Web 后,请参阅 iOS 网络特定安装说明。在 iOS 上测试网络应用程序与在其他任何网络平台上测试相同。有关移动设备上网络测试的详细信息,请参阅移动设备上的浏览器,有关 Squish for Web 的教程请参阅教程:开始测试 Web 应用程序

Squish 内置了 IDE 和命令行工具。使用 squishide 是开始测试的最简单和最佳方式,但一旦您构建了大量的测试,您可能希望自动运行它们。例如,进行回归测试套件的夜间运行。因此,了解如何使用可以从批处理文件或 shell 脚本运行的命令行工具是值得的。

注意:如果您需要视频指导,Qt Academy(Qt Academy)提供了一门关于 Squish 基本使用的 在线课程,课程时长 45 分钟。

在本章中,我们将使用一个简单的 Elements 应用程序作为我们的 AUT(受测试应用程序)。此应用程序与 Squish 一起在 SQUISHDIR/examples/ios/elements 中提供。这是一个非常基础的应用程序,显示了有关元素(氢、氦等)的信息,并允许用户通过名称或类别滚动查看元素或通过键入一些搜索文本来查找元素。尽管应用程序很简单,但它拥有大多数标准 iOS 应用程序的关键功能:可点击的按钮、可滚动的列表和用于输入文本的编辑框。您学习用于测试此应用程序的所有理念和做法都可以很容易地适应您自己的应用程序。有关测试各种 iOS 特定功能和标准编辑小部件的更多示例,请参阅如何创建测试脚本如何测试 iOS 应用程序

截图显示了应用程序运行的情况;左侧图像显示正在显示的元素,右侧图像显示了应用程序的主窗口。

"The iOS \c {Elements.app} example in the simulator"

使用示例

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

Squish 概念

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

要进行测试,您需要

  1. 要测试的应用程序,称为 受测试应用程序(AUT)。
  2. 一个测试脚本,用来执行 AUT。

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

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

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

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

"Squish tools"

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

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

"Squish IDE"

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

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

创建测试套件

测试套件是一组测试用例(test)。使用测试套件很方便,因为它可以轻松地将脚本和测试数据共享到一组相关的测试中。

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

squishide 创建测试套件

启动 squishide,通过单击或双击squishide图标,从任务栏菜单启动squishide或通过命令行执行squishide,任选其一,应用到您所使用的平台上即可。一旦Squish启动,您可能会看到 欢迎页面。点击右上角的工作台按钮使其消失。然后,squishide的界面将类似于截图,但可能因窗口系统、颜色、字体和主题的不同而略有差异。

"The Squish IDE with no Test Suites"

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

"Name and Directory page"

为您的测试套件输入一个名称,并选择存储测试套件的文件夹。在截图中,我们将测试套件命名为suite_py,并放入squish-ios-test文件夹中;实际的示例代码在Squish的examples/ios/elements文件夹中。(对于您自己的测试,您可能使用更有意义的名称,例如"suite_elements";我们选择"suite_py",因为为了教程的方便,我们将创建多个套件,一个用于每个Squish支持的脚本语言。)当然,您可以选择您喜欢的任何名称和文件夹。详细信息填写完毕后,点击 下一步 继续前往“工具箱(或脚本语言)”页面。

"Toolkit page"

如果您得到此向导页面,请单击您的自动测试程序(AUT)使用的工具箱。对于这个示例,您必须单击iOS,因为我们要测试iOS应用程序。然后单击 下一步 以进入脚本语言页面。

"Scripting Language page"

选择您想要的任意一种脚本语言。唯一的要求是每个测试套件只能使用一种脚本语言。要使用多种脚本语言,请创建多个测试套件,每个套件对应您想要使用的每种脚本语言。Squish为所有语言提供的功能都相同。选择了一种脚本语言后,请再次点击 下一步 以进入向导的最后一页。

"AUT page"

如果您正在为Squish已知的AUT创建新的测试套件,只需单击下拉列表以显示AUT列表,并选择您想要的AUT。如果下拉列表为空或您的AUT不在列表中,请单击下拉列表右侧的浏览按钮—这将显示一个文件打开对话框,您可以从中选择AUT。在iOS程序的情况下,AUT是应用程序的可执行文件(例如iOS上的Elements)。选择AUT后,单击 完成,Squish将创建一个与测试套件同名的子文件夹,并在该文件夹中创建一个名为suite.conf的文件,其中包含测试套件的配置详细信息。Squish还将AUT注册到squishserver。随后,向导将关闭,squishide将类似于下面的截图。

"The suite_py test suite"

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

从命令行创建测试套件

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

  1. 创建一个新的目录来存储测试套件—该目录的名称应以suite开头。在这个示例中,我们创建了用于Python测试的SQUISHDIR/examples/ios/elements/suite_py目录。我们也有类似的子目录用于其他语言,但这只是为了示例,因为通常我们只用一种语言进行所有测试。
  2. 将AUT注册到squishserver。

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

    这是通过在命令行中执行squishserver,并使用带有--config选项和addAUT命令来完成的。例如,假设我们在macOS上位于SQUISHDIR目录中

    squishserver --config addAUT Elements \
    SQUISHDIR/examples/ios/elements

    我们必须给addAUT命令提供AUT的可执行文件名称,并且——分开地提供AUT的路径。在这种情况下,路径指向在测试套件配置文件中添加为AUT的可执行文件。有关应用程序路径的更多信息,请参见AUTs and Settings。有关squishserver的命令行选项的更多信息,请参见squishserver

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

    AUT是iOS的可执行文件。根据Squish的安装方式,LANGUAGE可以设置为JavaScript、Python、Perl、Ruby或Tcl。WRAPPERS应设置为iOS,LAUNCHER设置为iphonelauncher

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

录制测试和校验点

Squish使用指定的测试套件脚本来记录测试。一旦录制了一个测试,我们就可以运行该测试,Squish会忠实地重复我们在录制测试时执行的所有操作,但不会包含人类易于犯的暂停,而计算机不需要这些暂停。我们也可以修改已录制的测试,或者将已录制测试的部分复制到手动创建的测试中,这将在教程的后续部分中看到。

录制将放入现有的测试用例中。您可以通过以下方式创建一个新脚本测试用例

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

将新测试用例命名为“tst_general”。

Squish会自动在测试套件文件夹内部创建一个同名子文件夹和一个测试文件,例如test.py。如果您选择JavaScript作为脚本语言,该文件名为test.js,其他相应地适用于Perl、Ruby或Tcl。

如果您收到的是.feature样本文件而不是“Hello World”脚本,请点击运行测试套件)左侧的箭头,然后选择新脚本测试用例)。

要将测试脚本文件(例如,test.jstest.py)显示在编辑视图中,根据“偏好设置” > “常规” > “打开模式”设置,单击或双击测试用例。这将选择脚本为主项并使相应的“记录” () 和“运行测试” () 按钮可见。

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

最初,脚本中的main()Hello World日志记录到测试结果中。要创建一个测试,就像我们在教程中稍后将要做的那样,我们必须创建一个main函数,并且应该在顶部导入相同的导入。对于Squish来说,main这个名称是特殊的。测试可以根据脚本语言支持的最多函数和其他代码来创建,但在测试执行时(也就是运行时),Squish总是执行main函数。您可以在测试脚本之间共享常用代码,如如何创建和使用共享数据及共享脚本中所述。

Squish还有两个特殊名称的函数:cleanupinit。更多详情请参阅 测试人员创建的特殊函数

创建新的测试用例后,我们就可以自由地手动编写测试代码或记录一个测试。单击测试用例的“记录” () 按钮会将测试的代码替换为新录制的内容。或者,您可以按照如何编辑和调试测试脚本中的说明来录制片段并存入现有测试用例中。

从命令行创建测试

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

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

记录我们的第一个测试

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

  1. 单击按名称选择元素选项。
  2. 单击Argon元素。
  3. 验证分类为“稀有气体”。
  4. 返回主窗口。
  5. 单击搜索。
  6. 输入搜索词“pluto”并单击搜索按钮。
  7. 验证元素94,钚被发现。
  8. 完成。

我们现在准备记录第一次测试。点击测试套件视图的“测试用例列表”中右侧的tst_general测试用例旁边的记录)来开始记录。这将导致Squish运行系统自动工具(AUT),以便您可以与之交互。一旦模拟器启动,且元素AUT正在运行,请执行以下操作——不必担心它会花费多长时间,因为Squish不记录闲置时间。

  1. 点击按名称选元素项。一旦出现元素列表,点击Argon (Ar)项。
  2. 当argon屏幕出现时,您想验证它是否具有正确的类别。为此验证,您将采取略微冗长的方法。首先,点击Squish 控制栏窗口中的验证工具栏按钮(从左起的第二个按钮)并选择属性

    {}

    这使得squishide重新出现。在应用程序对象视图中,展开元素项(通过单击其灰色三角形),然后展开UI_Window_0项,然后是UILayoutContainerView_0项,然后是UINavigationTransitionView_0项,然后是UIViewControllerWrapperView_0项,然后是UITableView_0项。现在应该可以看到表格的项。现在展开Category_UITableViewCell_8项,然后展开UITableViewCellContentView_0项。现在点击Noble Gases_UITableViewLabel_0项。最后我们找到了我们想要的项。(不必担心,当您进行下一次验证时,Squish将为您找到该项!)

  3. 在属性视图中,展开标签的text属性。现在点击stringValue子属性旁边的复选框。现在Squish应该看起来类似于截图。现在点击保存并插入验证按钮。这将把类别验证插入到记录脚本中。squishide将消失,您可以继续记录与AUT的交互。
  4. 回到元素AUT中,点击名称返回到名称元素列表,然后点击主窗口返回到主窗口。
  5. 点击搜索项,在搜索窗口的“名称包含”行编辑中输入文本“pluto”。然后点击搜索按钮。
  6. 当出现搜索结果时,您想验证是否找到了元素94,钚。这次,您将让Squish为您找到相关对象。再次单击Squish控制栏中的插入验证工具栏按钮并选择属性。和之前一样,这将使squishide出现。
  7. 应用程序对象视图中单击对象选择器)以使squishide消失。将鼠标悬停在搜索结果窗口中“94: 钚 (Pu)”文本上并单击该文本。squishide将现在重新出现,Squish将找到并突出显示相关小部件。
  8. 属性视图中,展开小部件的text属性。现在点击stringValue子属性旁边的复选框。现在Squish应该看起来类似于截图。现在点击保存并插入验证按钮。这将把验证插入到记录脚本中。squishide将消失,您可以继续记录与AUT的交互。
  9. 我们已经完成了测试并插入了验证。在 Squish 控制栏中点击 停止录制 工具栏按钮。自动化的元素和模拟器将停止,squishide 将重新出现。

一旦停止录制,记录的测试将如截图所示出现在 squishide 中。记录的确切代码将根据您如何与自动化的元素交互以及您选择的脚本语言而有所不同。

录制完成后,您可以通过点击测试用例视图中的 tst_general播放 按钮来播放它,以检查它是否符合预期。

"The results of playback with two verification points"

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

现在我们已经录入了测试,我们可以播放它,即运行它。如果播放失败,则可能意味着应用程序已被损坏。此外,正如截图所示,我们将进行的两个验证将在播放时进行检查。

在测试录制过程中插入验证点是十分方便的。在这里我们插入了两个,但在测试录制过程中我们可以随时插入尽可能多的验证。然而,有时我们可能会忘记插入验证,或者后来可能想要插入新的验证。正如我们将看到的那样,我们可以很容易在记录的测试脚本中插入额外的验证。

在进一步讨论之前,我们将探讨如何从命令行录制测试。然后我们将看到如何运行测试,并且我们还将查看 Squish 录制测试时生成的一些代码,并讨论其功能。

从命令行录制测试

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

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

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

最好使用 --useWaitFor 选项来录制对 Object waitForObject(objectOrName) 函数的调用,这比使用 snooze(seconds) 函数更可靠,尽管它因历史原因而是默认选项。默认情况下 squishide 将自动使用 Object waitForObject(objectOrName) 函数。

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

从 IDE 运行测试

要在 squishide 中运行测试用例,请将鼠标悬停在 测试套件视图 中选中的测试用例上时,点击出现的 运行测试)。

要一个接一个地运行两个或多个测试用例,或者仅运行选定的测试用例,请点击 运行测试套件)。

从命令行运行测试

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

要使用命令行回放已录制的测试,我们需要执行squishrunner程序,并指定我们的 Recorded 脚本所在的测试套件和要播放的测试用例。例如,假设我们位于包含测试套件目录的目录中

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

检查生成的代码

如果您查看截图中的代码(或下面的代码片段),您会看到它由许多Object waitForObject(objectOrName)调用来作为其他各种调用(如tapObject(objectOrName)type(objectOrName, text))的参数。该Object waitForObject(objectOrName)函数会等待GUI对象准备好交互(也就是说,变得可见和启用),然后其后会跟随一些与该对象交互的函数。典型的交互包括单击按钮或输入文本。

要全面了解Squish的脚本命令,请参阅如何创建测试脚本如何测试应用程序 - 详细说明API参考工具参考

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

注意:尽管截图只显示了Python测试套件的操作,但在此处和整个教程中引用的代码片段中,我们展示了所有Squish支持的计算脚本的语言。在实际操作中,您当然通常只会使用其中一种,因此请放心只查看您感兴趣的片段,并跳过其他片段。

import names


def main():
    startApplication("Elements")
    tapObject(waitForObject(names.elements_by_name_UITableViewCell))
    tapObject(waitForObject(names.argon_Ar_UITableViewCell))
    test.compare(waitForObjectExists(names.noble_Gases_UILabel).text.stringValue, "Noble Gases")
    tapObject(waitForObject(names.name_UINavigationItemButtonView))
    snooze(1)
    tapObject(waitForObject(names.main_UINavigationItemButtonView))
    tapObject(waitForObject(names.search_UITableViewCell))
    tapObject(waitForObject(names.o_UISearchBarTextField), 113, 14)
    type(waitForObject(names.o_UISearchBarTextField), "pluto")
    tapObject(waitForObject(names.search_UINavigationButton))
    test.compare(waitForObjectExists(names.o94_Plutonium_Pu_UITableViewCell).text.stringValue, "94: Plutonium (Pu)")
import * as names from 'names.js';

function main()
{
    startApplication("Elements");
    tapObject(waitForObject(names.elementsByNameUITableViewCell));
    tapObject(waitForObject(names.argonArUITableViewCell));
    test.compare(waitForObjectExists(names.nobleGasesUILabel).text.stringValue,
            "Noble Gases");
    tapObject(waitForObject(names.nameUINavigationItemButtonView));
    snooze(1);
    tapObject(waitForObject(names.mainUINavigationItemButtonView));
    tapObject(waitForObject(names.searchUITableViewCell));
    tapObject(waitForObject(names.uISearchBarTextField), 113, 14);
    type(waitForObject(names.uISearchBarTextField), "pluto");
    tapObject(waitForObject(names.searchUINavigationButton));
    test.compare(waitForObjectExists(names.o94PlutoniumPuUITableViewCell).text.stringValue,
            "94: Plutonium (Pu)");
}
require 'names.pl';

sub main {
    startApplication("Elements");
    tapObject(waitForObject($Names::elements_by_name_uitableviewcell));
    tapObject(waitForObject($Names::argon_ar_uitableviewcell));
    test::compare(waitForObjectExists($Names::noble_gases_uilabel)->text->stringValue,
        "Noble Gases");
    tapObject(waitForObject($Names::name_uinavigationitembuttonview));
    snooze(1);
    tapObject(waitForObject($Names::main_uinavigationitembuttonview));
    tapObject(waitForObject($Names::search_uitableviewcell));
    tapObject(waitForObject($Names::uisearchbartextfield), 113, 14);
    type(waitForObject($Names::uisearchbartextfield), "pluto");
    tapObject(waitForObject($Names::search_uinavigationbutton));
    test::compare(waitForObjectExists($Names::o94_plutonium_pu_uitableviewcell)->text->stringValue,
        "94: Plutonium (Pu)");
}
require 'names';

# encoding: UTF-8
require 'squish'
include Squish

def main
    startApplication("Elements")
    tapObject(waitForObject(Names::Elements_by_name_UITableViewCell))
    tapObject(waitForObject(Names::Argon_Ar_UITableViewCell))
    Test.compare(waitForObjectExists(Names::Noble_Gases_UILabel).text.stringValue,
                 "Noble Gases")
    tapObject(waitForObject(Names::Name_UINavigationItemButtonView))
    snooze(1)
    tapObject(waitForObject(Names::Main_UINavigationItemButtonView))
    tapObject(waitForObject(Names::Search_UITableViewCell))
    tapObject(waitForObject(Names::UISearchBarTextField), 113, 14)
    type(waitForObject(Names::UISearchBarTextField), "pluto")
    tapObject(waitForObject(Names::Search_UINavigationButton))
    Test.compare(waitForObjectExists(Names::O94_Plutonium_Pu_UITableViewCell).text.stringValue,
                 "94: Plutonium (Pu)")
end
source [findFile "scripts" "names.tcl"]

proc main {} {
    startApplication "Elements"
    invoke tapObject [waitForObject $names::Elements_by_name_UITableViewCell]
    invoke tapObject [waitForObject $names::Argon_Ar_UITableViewCell]
    test compare [property get [property get \
        [waitForObjectExists $names::Noble_Gases_UILabel] text] stringValue] "Noble Gases"
    invoke tapObject [waitForObject $names::Name_UINavigationItemButtonView]
    snooze 1
    invoke tapObject [waitForObject $names::Main_UINavigationItemButtonView]
    invoke tapObject [waitForObject $names::Search_UITableViewCell]
    invoke tapObject [waitForObject $names::UISearchBarTextField] 113 14
    invoke type [waitForObject $names::UISearchBarTextField] "pluto"
    invoke tapObject [waitForObject $names::Search_UINavigationButton]
    test compare [property get [property get \
        [waitForObjectExists $names::94_Plutonium_Pu_UITableViewCell] text] stringValue] \
        "94: Plutonium (Pu)"
}

这里引用了整个测试脚本,因为它非常简短。每个Squish测试都必须有一个main函数,这是Squish调用以开始测试的函数。在这里,记录的测试脚本以调用ApplicationContext startApplication(autName)函数的方式按标准方式开始。

函数调用的其余部分涉及回放记录的交互,在这种情况下,使用tapObject(objectOrName)type(objectOrName, text)函数单击小部件并输入文本。

Squish使用以names.前缀开始的变量来引用对象,这将它们标识为符号名称。每个变量都包含一个值,即相应的实际名称,它可以是基于字符串的,也可以实现为属性到值的键值映射。Squish支持几种命名方案,所有这些都可以在脚本中使用和混合。使用符号名称的优点是,如果应用程序发生变化,需要不同的名称,我们可以简单更新Squish的Object Map(它与符号名称到实际名称相关联),因此不必更改我们的测试脚本。有关Object Map的更多信息,请参阅Object MapObject Map视图

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

插入额外的验证点

在前一节中,我们看到了在录制测试脚本时插入验证点是多么简单。验证点还可以添加到现有的测试脚本中,通过设置断点并使用squishide,或者简单地通过编辑测试脚本并将对Squish测试函数的调用放入其中,如test.compare-functiontest.verify-function

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

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

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

在请求Squish插入验证点之前,最好确保我们有一个我们要验证的事物和时间的清单。我们可以向tst_general测试用例添加许多潜在的验证,但鉴于我们在这里的目的是简单地展示如何操作,我们只会做两件事情——我们将验证Argon元素的Symbol是"Ar"以及它的Number是18。我们将把这些验证放在我们在录制过程中引入的验证其Category之后。

要使用squishide插入验证点,我们首先在脚本(无论是录制的还是手动编写的)中放置一个断点,它位于我们要验证的点。

为了清晰起见,我们创建了一个新的测试,称为 tst_argon。首先我们点击了 squishideNew Script Test Case)按钮,然后我们对测试进行了重命名,最后我们将整个 tst_general 的代码复制粘贴到新的测试中。因此,到目前为止,这两个测试具有相同的代码,但我们将通过添加新的验证来修改 tst_argon 测试。(实际上,您可以直接将验证添加到现有的测试中。)

"The tst_argon test case with a breakpoint"

如上图所示,我们已在第9行设置了断点。这通过简单地 Ctrl+Click 行号,然后在上下文菜单中选择 Add Breakpoint 菜单项来完成。我们选择这一行是因为它紧跟在我们的记录阶段添加的第一个验证点之后,因此在这一点上,Argon 的详细信息将显示在屏幕上。如果您以不同的方式记录了测试,行号可能会有所不同。

在设置断点后,我们现在运行测试,就像通常一样,通过点击 Run Test 按钮或点击 Run > Run Test Case 菜单选项。与正常的测试运行不同,当到达断点时(即在第9行或您设置的任何行),测试将停止,Squish 的主窗口将重新出现(这可能会遮挡 AUT)。在这种情况下,squishide 将自动切换到 Test Debugging Perspective

视角和视图

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

squishide 提供以下视角

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

插入验证点

如图所示,当 Squish 在断点处停止时,squishide 将自动切换到 Test Debugging Perspective。该视角显示了 Variables viewEditor viewDebug viewApplication Objects viewProperties viewMethods viewTest Results view

要插入验证点,我们可以使用对象选择器)或展开应用程序对象视图中的树节点,直到找到我们想要验证的对象。在这个例子中,我们想验证Symbol的UILabel的文本,所以我们一直展开到UITableView,然后是Symbol的UITableViewCell。一旦我们选择了适当的UILabel,我们就展开它在属性视图中的文本,并检查stringValue子属性。

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

要添加验证点,我们必须点击验证点编辑器的保存并插入验证按钮。插入后,测试回放会停止:我们可以通过点击调试视图中的继续工具栏按钮(或按F8键)继续,或者通过点击终止工具栏按钮来停止。这是为了让我们能够输入更多的验证。在这个例子中,我们现在已经完成,所以可以继续或终止测试。

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

完成插入验证后,现在应禁用断点。只需Ctrl+Click断点并单击上下文菜单中的禁用断点菜单选项。我们现在准备在没有断点但具有验证点的情况下运行测试。点击运行测试按钮。这一次,我们将获得一些测试结果——如图所示,我们已扩展以显示它们的详细资料。(我们还选择了Squish插入以执行验证的代码行——请注意,代码结构与记录期间插入的代码结构相同。)

"The newly inserted verification point in action"

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

尽管我们可以编写与自动生成的代码风格完全相同风格的测试脚本代码,但通常以略有不同的风格执行事情更清晰、更容易,我们将在稍后解释这一点。

对于我们手动添加的验证,我们想要检查Argon的数字在相关的UILabel中是否为"18"。截图显示了我们输入以获取此新验证的两行代码以及运行测试脚本的结果。

"Manually entered verification point in action"

在手动编写脚本时,我们使用Squish的test模块函数来验证测试脚本执行过程中的某些点的条件。如图(以及下面的代码片段)所示,我们首先获取对我们感兴趣的对象的引用。使用Object waitForObject(objectOrName)函数是编写测试脚本的标准做法。此函数等待对象可用(即可见并可用的),然后返回对该对象的引用。(否则将超时并引发可捕获的异常。)然后我们使用此引用来访问项目的属性和方法——在本例中为UILabel的stringValue子属性——并使用test.compare-function函数验证该值是不是我们期望的值。

以下是所有Squish支持脚本的Argon验证代码。当然,您只需查看您将要用于自己测试的代码语言即可。

    test.compare(waitForObjectExists(names.noble_Gases_UILabel).text.stringValue, "Noble Gases")
    test.compare(waitForObjectExists(names.ar_UILabel).text.stringValue, "Ar")
    label = waitForObject(names.o18_UILabel)
    test.compare(label.text.stringValue, "18")
    test.compare(waitForObjectExists(names.nobleGasesUILabel).text.stringValue,
            "Noble Gases");
    test.compare(waitForObjectExists(names.arUILabel).text.stringValue, "Ar");
    var label = waitForObject(names.o18UILabel);
    test.compare(label.text.stringValue, "18")
    test::compare(waitForObjectExists($Names::noble_gases_uilabel)->text->stringValue,
        "Noble Gases");
    test::compare(waitForObjectExists($Names::ar_uilabel)->text->stringValue, "Ar");
    my $label = waitForObject($Names::o18_uilabel);
    test::compare($label->text->stringValue, "18");
    Test.compare(waitForObjectExists(Names::Noble_Gases_UILabel).text.stringValue,
                 "Noble Gases")
    Test.compare(waitForObjectExists(Names::Ar_UILabel).text.stringValue, "Ar")
    label = waitForObject(Names::O18_UILabel)
    Test.compare(label.text.stringValue, "18")
    test compare [property get [property get \
        [waitForObjectExists $names::Noble_Gases_UILabel] text] stringValue] "Noble Gases"
    test compare [property get [property get \
        [waitForObjectExists $names::Ar_UILabel] text] stringValue] "Ar"
    set label [waitForObject $names::18_UILabel]
    test compare [property get [property get $label text] stringValue] "18"

编码模式非常简单:我们获取对我们感兴趣的对象的引用,然后使用Squish的验证函数之一来验证其属性。(回想一下,我们是从剪贴板中粘贴UILabel的符号名称。)当然,如果我们希望,我们也可以调用对象上的方法与之交互。

对于完整覆盖所有验证点,请参阅如何创建和使用验证点

测试结果

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

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

Squish的测试结果接口非常灵活。通过实现自定义报告生成器,可以以许多不同的方式处理测试结果,例如将它们存储在数据库中,或将它们输出为HTML文件。默认报告生成器在Squish从命令行运行时简单地输出到stdout,或在使用{squishide}时输出到测试结果视图。您可以通过Ctrl+Click测试结果并选择Export Results菜单选项来将测试结果从{squishide}保存为XML。有关报告生成器的列表,请参阅生成报告。还有可能直接将测试结果记录到数据库中,如如何在Squish测试脚本中访问数据库中所述。

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

学习更多

我们现在已经完成了教程。Squish可以做很多多我们在这里展示的内容,但我们旨在让您尽快最简单地开始基本的测试。有关如何创建测试脚本以及如何使用特定API进行测试的示例,请参阅如何创建测试脚本如何测试应用程序——具体细节部分。

《API参考手册》和《工具参考手册》详细介绍了Squish的测试API及其提供的众多功能,旨在使测试尽可能简单高效。阅读《如何创建测试脚本》和《如何测试应用的特定细节》以及浏览《API参考手册》和《工具参考手册》都是非常有价值的,因为这样可以了解Squish能够为您提供的功能,并避免重复已经在系统中存在的功能。

关于在iOS模拟器中测试iOS应用的注意事项

Squish for iOS允许您在Xcode安装中包含的iOS模拟器中测试您的iOS应用。这使您在无需使用真实iOS设备的情况下测试iOS自动化测试对象(AUT)变得更加简单和方便。

  • 由于iOS模拟器是Xcode的一部分,因此您需要安装Xcode才能在模拟器中运行Squish测试。
  • 在iOS模拟器中,您只能运行为模拟器编译的应用程序,而不能运行为在设备上运行而编译的应用程序。所以请确保在测试套件向导中选择了iOS应用的正确版本作为AUT。
  • 您必须在Squish中以应用(即具有.app扩展名的文件)作为AUT。该.xcodeproj文件是包含应用构建信息的Xcode项目。该.xcodeproj文件不能用作AUT。

    使用Xcode打开.xcodeproj文件。在Product > Destination中选择一个模拟器目标,然后选择Product > Build For > Running来构建应用。根据Xcode的版本,步骤可能会有所不同,但Xcode 8.3会将.app文件放在~/Library/Developer/Xcode/DerivedData(在您的家目录下)的一个文件夹中。

测试套件中有其他选项可以让你控制如何启动iOS模拟器。要使用这些选项,请打开squishide中的测试套件设置,并将以下选项之一或多个输入到Launcher Arguments行编辑。

  • --device-id=uuid如果您使用Xcode 6或更高版本,您可以指定要使用的模拟设备的设备ID。

    在Xcode中,使用Window > Devices查看可用的模拟设备的设备ID。或者,在终端中运行Squish的iphonelauncher命令,并使用--list-devices选项来确定设备ID。

    由于设备ID已经定义了模拟硬件和SDK,因此不能与此选项一起使用--device--sdk选项,因为这些值不能覆盖。

  • --device=device-family如果您的应用程序是通用应用程序(即在iPhone和iPad上都运行),可以使用此选项指定Squish应启动模拟的iPhone或iPad。对于device-family,您可以使用iPhoneiPad

    如果您使用的是Xcode 5.0或更高版本,您将具有更精细地控制确切的设备类型,并且也可以将iPhone-retina-3.5-inchiPhone-retina-4-inchiPad-retina作为device-family

  • --sdk=version Squish尝试自动确定编译应用时使用的iOS SDK版本。如果失败或您想使用不同的SDK启动模拟器,请使用此选项覆盖自动确定的版本。

    例如,如果想让应用以SDK 4.2启动,指定选项--sdk=4.2

关于在iPhone或iPad测试iOS应用的方法

在真实的iPhone或iPad设备上测试iOS应用是可能的,尽管可能稍微不方便。为此,您必须将Squish特定的包装库添加到Xcode中,对您应用程序的main函数进行一些修改,并确保您的Mac正确设置。

您的台式电脑和iOS设备通过TCP/IP网络连接进行通信。该设备必须从台式电脑可访问,以便Squish可以连接到iOS设备上的AUT。

修改AUT的main函数

首先,您必须修改您的应用程序的main函数,使其在测试运行时调用Squish的squish_allowAttaching函数。以下是一个典型的用于iOS应用程序的main函数,其中包含必要的修改。根据您的具体源代码,main函数可能有所不同;您不应简单地复制以下代码;相反,应该修改现有的源代码,将突出显示的行添加到适当的位置。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#if defined(SQUISH_TESTING) && !TARGET_IPHONE_SIMULATOR
extern bool squish_allowAttaching(unsigned short port);
#endif

int main(int argc, char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

#if defined(SQUISH_TESTING) && !TARGET_IPHONE_SIMULATOR
    squish_allowAttaching(11233);
#endif

    int retVal = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    [pool release];
    return retVal;
}
  1. defined(SQUISH_TESTING)表示只有在定义了SQUISH_TESTING的情况下才会编译Squish特定的修改。我们稍后会设置Xcode项目,让特殊构建配置设置此编译器定义。因此,您可以在为Squish测试构建应用程序的版本和可以提交到应用商店的正常版本之间轻松切换。

    !TARGET_IPHONE_SIMULATOR表示我们仅在为设备(而非模拟器构建)构建时将Squish特定的修改编译到iOS应用程序中。

  2. 我们稍后需要在主函数中调用squish_allowAttaching函数。此函数在Squish提供的静态库中实现。因此,此处我们需要声明此函数,以便编译器知道当我们稍后尝试调用它时。您必须在调用该函数之前,在main函数之前添加此声明。
  3. 在创建autorelease池之后并在进入事件循环之前调用squish_allowAttaching(11233)。参数11233是Squish将使用的TCP/IP端口号,以连接到设备上运行的应用程序。

如果您有一个Swift-only项目,则缺少main函数。在这种情况下,只需为您的应用目标添加一个新的源文件squish_loading.c,内容如下所示:

#if defined(SQUISH_TESTING) && !TARGET_IPHONE_SIMULATOR

#include <stdbool.h>

extern bool squish_allowAttaching(unsigned short port);

__attribute__((constructor)) static void initializer()
{
    squish_allowAttaching(11233);
}

#endif

将包装库添加到Xcode

在修改了应用程序的main函数之后,我们还必须将应用程序链接到与Squish软件包一起提供的静态库libsquishioswrapper.a,可以在软件包的lib/arm目录中找到。

注意:以下步骤使用Xcode 7.3。不同的Xcode版本在用户界面步骤(尤其是屏幕截图)方面的确可能有所不同,但总体过程对所有Xcode版本都是相同的。

首先,我们在Xcode项目中创建一个新的构建配置。这使我们能够轻松地在您的应用程序的Squish构建和正常构建之间切换(无需Squish所需的修改):单击项目以打开项目设置。在项目设置中的Info选项卡中,您可以选择复制现有的构建配置。您可以在任何现有的构建配置上创建构建;在我们的示例中,我们选择复制“发布”构建配置(即,我们将Squish特定的配置基于发布构建)。

"Duplicate the Release build configuration"

给新的构建配置起一个名字,在我们的示例中我们简单地选择“Squish”。

"Name the new build configuration Squish"

接下来,我们需要确保在用“Squish”构建配置(这是我们修改后的 main 函数中检查的宏)构建项目时,编译器定义了 SQUISH_TESTING。

  1. 选择您想要用 Squish 测试的目标应用程序(在示例中是 Elements 目标)。
  2. 切换到项目设置中的 构建设置 选项卡。
  3. 确保显示 所有 构建设置(而不仅仅是 基本 设置)。
  4. 搜索“其他 C 标志”构建设置。
  5. 确保展开 其他 C 标志 条目。
  6. 选择 Squish 构建配置。
  7. 双击目标列(示例中的 Elements 列)中 Squish 构建配置的 其他 C 标志 条目。
  8. 在弹出窗口中点击 + 按钮以添加新的标志。
  9. 输入标志 -DSQUISH_TESTING 并在弹出窗口外点击以接受新的设置。

"Extend the compiler flags for the Squish configuration"

然后,我们还需要将 Squish 静态库添加到链接器标志中。

  1. 搜索“其他链接器标志”构建设置。
  2. 确保展开 其他链接器标志
  3. 选择 Squish 构建配置。
  4. 双击目标列(示例中的 Elements 列)中 Squish 构建配置的 其他链接器标志 条目。
  5. 在弹出窗口中点击 + 按钮以添加新的标志。
  6. 输入以下标志(顺序很重要)
    • -lc++
    • -liconv
    • -lz
    • -force_load
    • squishdir/lib/arm/libsquishioswrapper.a

    并在弹出窗口外点击以接受更改。确保将 <squishdir> 替换为您的 Squish 安装目录的完整路径(或相对路径)。或者,您也可以将库复制到应用程序的项目目录中,并在不添加任何路径的情况下指定 libsquishioswrapper.a

注意: 当您更新 Squish 安装到新版本时,使用新包中的 libsquishioswrapper.a 库,并使用库的新版本重新构建您的应用程序。

"Extend the linker flags for the Squish configuration"

然后,您必须禁用“Squish”配置的位码支持

  1. 搜索“位码”构建设置。
  2. 确保展开 启用位码
  3. 选择 Squish 构建配置。
  4. 将目标列(示例中的 Elements 列)中 Squish 构建配置的设置更改为

"Disable bitcode support for the Squish configuration"

最后一步是真正使用新创建的“Squish”配置构建 iOS 应用程序。为此,我们在 Xcode 中创建一个单独的计划。这允许我们快速在针对 Squish 测试和其他应用程序目的之间切换。

从 Xcode 中的计划弹出菜单中选择 新建计划

"Create a new scheme for "Squish" builds"

为新创建的计划提供一个好名字,例如我们使用“Elements (Squish)”,强调这是构建 Elements 应用程序以供 Squish 测试。

"Name the new scheme"

新创建的计划具有默认设置。所以现在我们需要编辑计划并将用于的构建配置更改。所以请确保该新计划是活动的,并从弹出菜单中选择 编辑计划

"Edit the new scheme"

在编辑计划的对话框中,确保您选择了 运行 动作。然后,将 构建配置 设置更改为 Squish。您应该对也接受构建配置的其他操作执行相同操作(即,对于 测试分析归档)。因此,“Elements (Squish)”方案上完成的任何构建都将构建适合用 Squish 测试的应用程序。

"Let the new scheme build the Squish configuration"

现在您只需要为您自己的设备构建应用程序,并在那里安装它,您就可以开始在物理设备上测试它了(在您的台式电脑上设置测试套件之后的最后几个步骤)。

为了快速测试上述所有修改是否正确,请通过Xcode的调试器在设备上执行应用程序。请仔细查看Xcode的调试器控制台:如果应用程序启动时看到消息“Listening on port 11233 for incoming connections”,则修改正确。如果您没有看到此消息,则您错过了上述步骤中的一个。

为iOS设备测试设置计算机

尽管您想要测试的iOS应用程序将在iOS设备上运行,但Squish本身运行在计算机上。以下是设置计算机以支持iOS测试的步骤。

  • 您必须关闭计算机上的防火墙。当然,测试完成后,您必须将防火墙重新打开。
  • 将iOS设备的计算机和端口号注册为可附加的AUT。这可以通过在squishide内部完成;点击编辑 > 服务 > 设置 > 管理AUT菜单项,然后点击可附加AUT项。现在点击添加按钮。为配置命名,例如,“iPhoneDevice”。将iOS设备的IP地址作为主机输入,并为端口号输入调用squish_allowAttaching函数时使用的数字(例如,11233)。

现在计算机已设置好,您可以为您的iOS应用程序播放或创建测试。

如果您想要播放使用模拟器创建的测试,您必须将测试脚本中的startApplication("iPhoneApp")更改为attachToApplication("iPhoneDevice")(或者如果不同,使用您选择的任何配置名称)。

现在您可以在设备上启动应用程序,然后在iOS模拟器上回放您记录的测试脚本。

您也可以直接在设备上录制测试。在这种情况下,请打开iOS测试套件的测试套件设置,并确保AUT选择为<没有应用程序>。然后在设备上启动应用程序,如果您在squishide中选择录制测试用例,请选择应用程序。选择iPhoneDevice(可附加的)(或无论您在注册可附加AUT时使用什么名称)。\n\n现在,您在设备上进行的所有用户交互都会记录下来,直到您在squishide的控制栏中结束记录。

iOS设备基本上被锁定,因此Squish无法启动(或结束)AUT。因此,必须手动启动应用程序,并且在执行测试脚本时,确保应用程序在前台运行,且设备未锁定或处于睡眠状态。

如果您保留应用程序运行状态,则可以在每个测试用例之后执行多个测试用例;每个测试用例连接到同一应用程序。这意味着您必须确保在测试用例中应用程序处于下一个测试用例可以成功运行的状态(或者您必须以某种方式编写您的测试用例,以使它们在启动时将应用程序带入一个已知状态)。

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

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

在本章中,我们将使用Elements应用作为测试的应用程序(AUT)。此应用搜索并显示化学元素信息。您可以在Squish的examples/ios目录中找到它。截图显示了应用程序在运行中的样子。

"The iOS Elements example"

行为驱动开发简介

行为驱动开发(BDD)是测试驱动开发(TDD)的扩展,它将验收标准定义放在开发过程的开始,而不仅仅是在软件开发之后编写测试。测试后可能还会有代码更改周期。

"BDD process"

行为驱动测试由一组特性文件构建而成,通过一个或多个场景描述产品特性。每个场景由一系列代表该场景需要测试的动作或验证的步骤组成。

BDD侧重于预期的应用程序行为,而不是实现细节。因此,BDD测试以可读的领域特定语言(DSL)描述。由于这种语言不是技术的,因此不仅程序员,产品所有者、测试员或业务分析师也可以创建此类测试。此外,在产品开发期间,此类测试充当着活产品文档。对于Squish的用途,应使用Gherkin语法创建BDD测试。以前编写的产品规范(BDD测试)可以转换为可执行测试。本分步教程介绍使用squishide支持自动化的BDD测试。

Gherkin语法

Gherkin文件通过一个或多个场景中的预期应用程序行为描述产品特性。以下是一个展示元素示例应用程序搜索功能的例子。

Feature: Searching for elements
    As a user I want to search for elements and get correct results.

    Scenario: Initial state of the search view
        Given elements application is running
        When I switch to the search view
        Then the search field is empty

以上大部分是自由文本(不必是英文)。只要固定的是特性/场景结构和前缀关键字,如给定并且然后。这些关键字中的每一个都标志着一步步,定义前置条件、用户动作或预期结果。上述应用程序行为描述可以传递给软件开发人员以实现这些特性,同时也可以传递给软件测试人员以实现自动化测试。

测试实现

创建测试套件

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

创建测试用例

Squish提供了两种测试用例类型:"脚本测试用例" 和 "BDD测试用例"。由于"脚本测试用例"是默认选项,因此创建新的"BDD测试用例"需要使用上下文菜单,通过点击新建测试用例按钮旁边的展开器,并选择新建BDD测试用例选项。当您将来点击按钮时,squishide将记住您的选择,"BDD测试用例"将成为默认选项。

"Creating new BDD Test Case"

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

我们需要将Gherkin模板替换为针对addressbook示例应用程序的Feature。为此,复制下面的Feature描述,并将其粘贴到Feature文件中。

Feature: Searching for elements
    As a user I want to search for elements and get correct results.

    Scenario: Initial state of the search view
        Given elements application is running
        When I switch to the search view
        Then the search field is empty

在编辑test.feature文件时,对于每个未定义的步骤,会显示一个警告未找到实现。实现代码在steps子目录中,在测试用例资源中,或在测试套件资源中。现在运行我们的Feature测试将目前在第一步失败,出现"No Matching Step Definition",接下来的步骤将被跳过。

录制步骤实现

为了录制Scenario,请按录制按钮旁边相应的Scenario,该按钮列出在测试用例资源视图中的Scenarios标签页。

"Record Scenario"

这将导致Squish运行AUT,以便我们可以与之交互。此外,还会显示控制栏,其中列出所有需要录制的步骤。现在,AUT或添加到脚本的任何验证点都与控制栏上的第一个步骤Given elements application is running(该步骤在控制栏的步骤列表中被加粗)交互。由于Squish自动录制应用程序的开始,我们的第一步已经完成了。

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

"Control Bar"

现在,步骤When I switch to the search view是活动的。通过在AUT中点击搜索菜单项来录制必要的动作。再次点击控制栏当前步骤前面的箭头按钮(完成录制步骤))将进行最后一步的录制,the search field is empty。在录制时点击验证并选择属性。在应用程序对象视图中,导航或使用对象选择器)选择(不勾选)搜索文本字段。现在展开小部件的text属性。现在,在stringValue子属性旁边点击复选框并插入验证点。最后,点击控制栏上的最后一个完成录制步骤)箭头按钮。

"Inserting Verification Point"

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

@Given("elements application is running")
def step(context):
    startApplication("Elements")

@When("I switch to the search view")
def step(context):
    tapObject(waitForObject(names.search_UILabel), 179, 9)

@Then("the search field is empty")
def step(context):
    test.compare(waitForObjectExists(names.o_UISearchBarTextField).text, "")
Given("elements application is running", function(context) {
    startApplication("Elements");
});

When("I switch to the search view", function(context) {
    tapObject(waitForObject(names.searchUILabel), 179, 9);
});

Then("the search field is empty", function(context) {
    test.compare(waitForObjectExists(names.uISearchBarTextField).text, "");
});
Given("elements application is running", sub {
    my $context = shift;
    startApplication("Elements");
});

When("I switch to the search view", sub {
    my $context = shift;
    tapObject(waitForObject($Names::search_uilabel), 179, 9);
});

Then("the search field is empty", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::uisearchbartextfield)->text, "");
});
Given("elements application is running") do |context|
    startApplication("Elements")
end

When("I switch to the search view") do |context|
    tapObject(waitForObject(Names::Search_UILabel), 179, 9)
end

Then("the search field is empty") do |context|
    Test.compare(waitForObjectExists(Names::UISearchBarTextField).text, "")
end
Given "elements application is running" {context} {
    startApplication "Elements"
}

When "I switch to the search view" {context} {
    invoke tapObject [waitForObject $names::Search_UILabel] 179 9
}

Then "the search field is empty" {context} {
    test compare [property get [waitForObjectExists $names::UISearchBarTextField] text] ""
}

应用程序在第一个步骤的开始处自动启动,因为记录了startApplication()调用。在每个Scenario的末尾,会调用OnScenarioEnd钩子,导致在应用程序上下文中调用detach(),从而使应用于终止。此钩子函数位于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 searching with exact match
    Given elements application is running
    When I switch to the search view
    And I enter 'helium' into the search field and tap Search
    Then '1' entries should be present

自动保存Feature文件后,squishide提示只须实现2个步骤:然后我输入'helium'搜索字段并触摸搜索然后应有1个条目存在。其余步骤已有匹配的步骤实现。

要记录缺少的步骤,请单击测试套件视图中测试用例名称旁边的记录)。脚本将播放直到到达缺少的步骤,然后提示您实现它。单击完成记录步骤)以转到下一个步骤。对于第二个缺少的步骤,我们可以记录一个对象属性验证,就像我们对搜索字段为空步骤所做的那样。

现在我们通过替换值来参数化生成的步骤实现,将值替换为参数类型。由于我们想要能够添加不同的名称,将'helium'替换为'|word|'。每个参数将按步骤描述名称中出现顺序传递给步骤实现函数。完成参数化后,编辑代码使其看起来像以下示例步骤

@When("I enter '|word|' into the search field and tap Search")
def step(context, search):
    tapObject(waitForObject(names.o_UISearchBarTextField), 25, 13)
    type(waitForObject(names.o_UISearchBarTextField), search)
    tapObject(waitForObject(names.search_UINavigationButton))

    # synchronization: wait until search result view is visible
    waitFor("not object.exists(names.o_UISearchBarTextField)", 10000)

    context.userData["search"] = search

@Then("'|integer|' entries should be present")
def step(context, numOfEntries):
    test.compare(waitForObjectExists(names.o_UITableView).numberOfRowsInSection_(0), numOfEntries)
When("I enter '|word|' into the search field and tap Search", function(context, search) {
    tapObject(waitForObject(names.uISearchBarTextField), 25, 13);
    type(waitForObject(names.uISearchBarTextField), search);
    tapObject(waitForObject(names.searchUINavigationButton));

    // synchronization: wait until search result view is visible
    waitFor("!object.exists(names.uISearchBarTextField)", 10000);

    context.userData["search"] = search;
});

Then("'|integer|' entries should be present", function(context, numOfEntries) {
    test.compare(waitForObjectExists(names.uITableView).numberOfRowsInSection_(0), numOfEntries);
});
When("I enter '|word|' into the search field and tap Search", sub {
    my $context = shift;
    my $search = shift;
    tapObject(waitForObject($Names::uisearchbartextfield), 25, 13);
    type(waitForObject($Names::uisearchbartextfield), $search);
    tapObject(waitForObject($Names::search_uinavigationbutton));

    # synchronization: wait until search result view is visible
    waitFor("!object::exists(\$Names::uisearchbartextfield)", 10000);

    $context->{userData}{"search"} = $search;
});

Then("'|integer|' entries should be present", sub {
    my $context = shift;
    my $numOfEntries = shift;
    test::compare(waitForObjectExists($Names::o_uitableview)->numberOfRowsInSection_(0), $numOfEntries);
});
When("I enter '|word|' into the search field and tap Search") do |context, search|
    tapObject(waitForObject(Names::O_UISearchBarTextField), 25, 13)
    type(waitForObject(Names::O_UISearchBarTextField), search)
    tapObject(waitForObject(Names::Search_UINavigationButton))

    # synchronization: wait until search result view is visible
    waitFor("!Squish::Object.exists(Names::O_UISearchBarTextField)", 10000);

    context.userData[:search] = search
end

Then("'|integer|' entries should be present") do |context, numOfEntries|
    Test.compare(waitForObjectExists(Names::O_UITableView).numberOfRowsInSection_(0), numOfEntries)
end
When "I enter '|word|' into the search field and tap Search" {context search} {
    invoke tapObject [waitForObject $names::UISearchBarTextField] 25 13
    invoke type [waitForObject $names::UISearchBarTextField] $search
    invoke tapObject [waitForObject $names::Search_UINavigationButton]

    # synchronization: wait until search result view is visible
    waitFor { ![object exists $names::UISearchBarTextField] } 10000

    set userData [$context userData]
    dict set userData "search" $search
    $context userData $userData
}

Then "'|integer|' entries should be present" {context numOfEntries} {
    test compare [invoke [waitForObjectExists $names::UITableView] numberOfRowsInSection_ 0] $numOfEntries
}

在表中为步骤提供参数

下一个Scenario将测试检索到多个元素的搜索结果。而不是为此使用多个步骤进行验证,我们使用一个步骤,并将表作为参数传递给步骤。

Scenario: State after searching with multiple matches
    Given elements application is running
    When I switch to the search view
    And I enter 'he' into the search field and tap Search
    Then the following entries should be present
        | Number | Symbol | Name          |
        | 2      | He     | Helium        |
        | 44     | Ru     | Ruthenium     |
        | 75     | Re     | Rhenium       |
        | 104    | Rf     | Rutherfordium |
        | 116    | Uuh    | Ununhexium    |

处理此类表的步骤实现看起来如下

@Then("the following entries should be present")
def step(context):
    table = context.table
    table.pop(0) # Drop initial row with column headers

    tableView = waitForObject(names.o_UITableView)
    dataSource = tableView.dataSource
    numberOfRows = tableView.numberOfRowsInSection_(0)

    test.compare(numberOfRows, len(table))
    for i in range(numberOfRows):
        number = table[i][0]
        symbol = table[i][1]
        name = table[i][2]
        expectedText = number + ": " + name + " (" + symbol + ")"

        indexPath = NSIndexPath.indexPathForRow_inSection_(i, 0)
        cell = dataSource.tableView_cellForRowAtIndexPath_(tableView, indexPath)
        test.compare(cell.text, expectedText)
Then("the following entries should be present", function(context) {
    var table = context.table;
    table.shift(); // Drop initial row with column headers

    var tableView = waitForObject(names.uITableView);
    var dataSource = tableView.dataSource;
    var numberOfRows = tableView.numberOfRowsInSection_(0);

    test.compare(numberOfRows, table.length);
    for (var i = 0; i < table.length; ++i) {
        var number = table[i][0];
        var symbol = table[i][1];
        var name = table[i][2];
        var expectedText = number + ": " + name + " (" + symbol + ")";

        var indexPath = NSIndexPath.indexPathForRow_inSection_(i, 0);
        var cell = dataSource.tableView_cellForRowAtIndexPath_(tableView, indexPath);
        test.compare(cell.text, expectedText);
    }
});
Then("the following entries should be present", sub {
    my $context = shift;
    my $table = $context->{'table'};
    shift(@{$table}); # Drop initial row with column headers

    my $tableView = waitForObject($Names::o_uitableview);
    my $dataSource = $tableView->dataSource;
    my $numberOfRows = $tableView->numberOfRowsInSection_(0);

    test::compare($numberOfRows, scalar @{$table});
    for (my $i = 0; $i < @{$table}; $i++) {
        my $number = @{@{$table}[$i]}[0];
        my $symbol = @{@{$table}[$i]}[1];
        my $name = @{@{$table}[$i]}[2];
        my $expectedText = $number . ": " . $name . " (" . $symbol . ")";

        my $indexPath = NSIndexPath::indexPathForRow_inSection_($i, 0);
        my $cell = $dataSource->tableView_cellForRowAtIndexPath_($tableView, $indexPath);
        test::compare($cell->text, $expectedText);
    }
});
Then("the following entries should be present") do |context|
    table = context.table
    table.shift # Drop initial row with column headers

    tableView = waitForObject(Names::O_UITableView)
    dataSource = tableView.dataSource
    numberOfRows = tableView.numberOfRowsInSection_(0)

    Test.compare(numberOfRows, table.length)
    for i in 0...numberOfRows do
        number = table[i][0]
        symbol = table[i][1]
        name = table[i][2]
        expectedText = number + ": " + name + " (" + symbol + ")"

        indexPath = NSIndexPath.indexPathForRow_inSection_(i, 0)
        cell = dataSource.tableView_cellForRowAtIndexPath_(tableView, indexPath)
        Test.compare(cell.text, expectedText)
    end
end
Then "the following entries should be present" {context} {
    # Drop initial row with column headers
    set table [$context table]
    set table [lrange $table 1 end]

    set tableView [waitForObject $names::UITableView]
    set dataSource [property get $tableView dataSource]
    set numberOfRows [invoke $tableView numberOfRowsInSection_ 0]

    test compare $numberOfRows [llength $table]
    for {set i 0} {$i < $numberOfRows} {incr i} {
        set number [lindex $table $i 0]
        set symbol [lindex $table $i 1]
        set name [lindex $table $i 2]
        set expectedText "$number: $name ($symbol)"

        set indexPath [invoke NSIndexPath indexPathForRow_inSection_ $i 0]
        set cell [invoke $dataSource tableView_cellForRowAtIndexPath_ $tableView $indexPath]
        test compare [property get $cell text] $expectedText
    }
}

在不同步骤和情景之间共享数据

让我们向Feature文件添加一个新Scenario。这一次,我们想检查在详细搜索结果中,详情视图的标题与我们的搜索词相同。由于我们在一个步骤中输入数据,在另一个步骤中进行验证,我们必须在那些步骤之间共享有关输入数据的信息,以执行验证。

Scenario: State of the details when searching
    Given elements application is running
    When I switch to the search view
    And I enter 'Carbon' into the search field and tap Search
    And I tap on the first search result
    Then the previously entered search term is the title of the view

要共享此数据,可以使用上下文.userData属性。

@When("I enter '|word|' into the search field and tap Search")
def step(context, search):
    tapObject(waitForObject(names.o_UISearchBarTextField), 25, 13)
    type(waitForObject(names.o_UISearchBarTextField), search)
    tapObject(waitForObject(names.search_UINavigationButton))

    # synchronization: wait until search result view is visible
    waitFor("not object.exists(names.o_UISearchBarTextField)", 10000)

    context.userData["search"] = search
When("I enter '|word|' into the search field and tap Search", function(context, search) {
    tapObject(waitForObject(names.uISearchBarTextField), 25, 13);
    type(waitForObject(names.uISearchBarTextField), search);
    tapObject(waitForObject(names.searchUINavigationButton));

    // synchronization: wait until search result view is visible
    waitFor("!object.exists(names.uISearchBarTextField)", 10000);

    context.userData["search"] = search;
});
When("I enter '|word|' into the search field and tap Search", sub {
    my $context = shift;
    my $search = shift;
    tapObject(waitForObject($Names::uisearchbartextfield), 25, 13);
    type(waitForObject($Names::uisearchbartextfield), $search);
    tapObject(waitForObject($Names::search_uinavigationbutton));

    # synchronization: wait until search result view is visible
    waitFor("!object::exists(\$Names::uisearchbartextfield)", 10000);

    $context->{userData}{"search"} = $search;
});
When("I enter '|word|' into the search field and tap Search") do |context, search|
    tapObject(waitForObject(Names::O_UISearchBarTextField), 25, 13)
    type(waitForObject(Names::O_UISearchBarTextField), search)
    tapObject(waitForObject(Names::Search_UINavigationButton))

    # synchronization: wait until search result view is visible
    waitFor("!Squish::Object.exists(Names::O_UISearchBarTextField)", 10000);

    context.userData[:search] = search
end
When "I enter '|word|' into the search field and tap Search" {context search} {
    invoke tapObject [waitForObject $names::UISearchBarTextField] 25 13
    invoke type [waitForObject $names::UISearchBarTextField] $search
    invoke tapObject [waitForObject $names::Search_UINavigationButton]

    # synchronization: wait until search result view is visible
    waitFor { ![object exists $names::UISearchBarTextField] } 10000

    set userData [$context userData]
    dict set userData "search" $search
    $context userData $userData
}

给定Feature中的所有步骤和Hooks都可以访问存储在上下文.userData中的所有数据。最后,我们需要实现步骤然后之前输入的搜索词是视图的标题

@Then("the previously entered search term is the title of the view")
def step(context):
    # synchronization: wait until the search result view is not visible
    waitFor('waitForObjectExists(names.o_UINavigationItemView).title != "Search Results"', 10000)

    test.compare(waitForObjectExists(names.o_UINavigationItemView).title, context.userData["search"])
Then("the previously entered search term is the title of the view", function(context) {
    // synchronization: wait until the search result view is not visible
    waitFor('waitForObjectExists(names.uINavigationItemView).title != "Search Results"', 10000);

    test.compare(waitForObjectExists(names.uINavigationItemView).title, context.userData["search"]);
});
Then("the previously entered search term is the title of the view", sub {
    my $context = shift;

    # synchronization: wait until the search result view is not visible
    waitFor("waitForObjectExists(\$Names::o_uinavigationitemview).title ne \"Search Results\"", 10000);

    test::compare(waitForObjectExists($Names::o_uinavigationitemview)->title, $context->{userData}{"search"});
});
Then("the previously entered search term is the title of the view") do |context|
    # synchronization: wait until the search result view is not visible
    waitFor('waitForObjectExists(Names::O_UINavigationItemView).title != "Search Results"', 10000)

    Test.compare(waitForObjectExists(Names::O_UINavigationItemView).title, context.userData[:search])
end
Then "the previously entered search term is the title of the view" {context} {
    # synchronization: wait until the search result view is not visible
    waitFor { [property get [waitForObjectExists $names::UINavigationItemView] title] != "Search Results" } 10000

    test compare [property get [waitForObjectExists $names::UINavigationItemView] title] [dict get [$context userData] "search"]
}

情景概述

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

Scenario: State after searching with exact match
    Given elements application is running
    When I switch to the search view
    And I enter 'Hydrogen' into the search field and tap Search
    Then the entry '1: Hydrogen (H)' should be present

Scenario: State after searching with exact match
    Given elements application is running
    When I switch to the search view
    And I enter 'Helium' into the search field and tap Search
    Then the entry '2: Helium (He)' should be present

正如我们所见,那些场景使用不同的测试数据执行相同的行为。同样可以通过使用场景概述(一个带有占位符的场景模板)和示例(一个参数表)来实现。

Scenario Outline: Doing a search with exact match multiple times
  Given elements application is running
  When I switch to the search view
  And I enter '<Name>' into the search field and tap Search
  Then the entry '<Number>: <Name> (<Symbol>)' should be present
  Examples:
     | Name     | Number | Symbol |
     | Hydrogen | 1      | H      |
     | Helium   | 2      | He     |
     | Carbon   | 6      | C      |

场景概述的每次循环迭代结束时,将执行OnScenarioEnd钩子。

测试执行

squishide中,用户可以执行特征中的所有场景,或仅执行选定的一个场景。为了执行所有场景,必须通过点击测试套件视图中的播放按钮来执行适当的测试用例。

"Execute all Scenarios from Feature"

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

"Execute one Scenario from Feature"

执行场景后,根据执行结果用颜色标记特征文件。更详细的信息(如日志)可以在测试结果视图中找到。

"Execution results in Feature file"

测试调试

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

"Breakpoint in Feature file"

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

重用步骤定义

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

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

本章针对有现有Squish脚本测试的用户,他们希望引入行为驱动测试。第一部分描述了如何保留现有测试并用BDD方法添加新测试。第二部分描述如何将现有测试转换为BDD。

将现有测试扩展到BDD

第一种方法是保留任何现有Squish测试,并通过添加新的BDD测试来扩展它们。可以有一个包含基于脚本的测试用例和BDD测试用例的测试套件。只需打开一个包含测试用例的现有测试套件,然后从下拉列表中选择新建BDD测试用例选项。

"Creating new BDD Test Case"

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

def switchToSearchView():
    tapObject(waitForObject(":Search_UITableViewLabel"), 179, 9)
function switchToSearchView(){
    tapObject(waitForObject(":Search_UITableViewLabel"), 179, 9);
}
sub switchToSearchView{
    tapObject(waitForObject(":Search_UITableViewLabel"), 179, 9);
}
def switchToSearchView
  tapObject(waitForObject(":Search_UITableViewLabel"), 179, 9)
end
proc switchToSearchView {} {
    invoke tapObject [waitForObject ":Search_UITableViewLabel"] 179 9
}

新的BDD测试用例可以方便地使用相同的函数。

@When("I switch to the search view")
def step(context):
    switchToSearchView()
When("I switch to the search view", function(context) {
    switchToSearchView();
});
When("I switch to the search view", sub {
    my $context = shift;
    switchToSearchView();
});
When("I switch to the search view") do |context|
    switchToSearchView()
end
When "I switch to the search view" {context} {
    switchToSearchView
}

将现有测试转换为BDD

第二种方案是将包含基于脚本的测试 测试套件 转换为行为驱动测试。由于 测试套件 可以包含基于脚本的测试用例和行为驱动测试用例,迁移可以是逐步进行的。包含这两种测试用例类型混合的 测试套件 可以无需额外努力即可执行,并分析结果。

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

"Conversion Chart"

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

def main():
    startApplication("Elements")
    tapObject(waitForObject(names.search_UILabel), 179, 9)
    test.compare(waitForObjectExists(names.o_UISearchBarTextField).text, "")
function main() {
    startApplication("Elements");
    tapObject(waitForObject(names.searchUILabel), 179, 9);
    test.compare(waitForObjectExists(names.uISearchBarTextField).text, "");
}
sub main {
    startApplication("Elements");
    tapObject(waitForObject($Names::search_uilabel), 179, 9);
    test::compare(waitForObjectExists($Names::uisearchbartextfield)->text, "");
}
def main
    startApplication("Elements")
    tapObject(waitForObject(Names::Search_UILabel), 179, 9)
    test.compare(waitForObjectExists(Names::UISearchBarTextField).text, "")
end
proc main {} {
    startApplication "Elements"
    invoke tapObject [waitForObject $names::Search_UILabel] 179 9
    test compare [property get [waitForObjectExists $names::UISearchBarTextField] text] ""
}

分析上述测试用例后,我们可以创建以下 场景 并将其添加到 test.feature

Scenario: Initial state of the search view
    Given elements application is running
    When  I switch to the search view
    Then  the search field is empty

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

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

@When("I switch to the search view")
def step(context):
    test.warning("TODO implement I switch to the search view")

@Then("the search field is empty")
def step(context):
    test.warning("TODO implement the search field is empty")
Given("elements application is running", function(context) {
    test.warning("TODO implement elements application is running");
});

When("I switch to the search view", function(context) {
    test.warning("TODO implement I switch to the search view");
});

Then("the search field is empty", function(context) {
    test.warning("TODO implement the search field is empty");
});
Given("elements application is running", sub {
    my $context = shift;
    test::warning("TODO implement elements application is running");
});

When("I switch to the search view", sub {
    my $context = shift;
    test::warning("TODO implement I switch to the search view");
});

Then("the search field is empty", sub {
    my $context = shift;
    test::warning("TODO implement the search field is empty");
});
Given("elements application is running") do |context|
    Test.warning "TODO implement elements application is running"
end

When("I switch to the search view") do |context|
    Test.warning "TODO implement I switch to the search view"
end

Then("the search field is empty") do |context|
    Test.warning "TODO implement the search field is empty"
end
Given "elements application is running" {context} {
    test warning "TODO implement elements application is running"
}

When "I switch to the search view" {context} {
    test warning "TODO implement I switch to the search view"
}

Then "the search field is empty" {context} {
    test warning "TODO implement the search field is empty"
}

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

@Given("elements application is running")
def step(context):
    startApplication("Elements")

@When("I switch to the search view")
def step(context):
    tapObject(waitForObject(names.search_UILabel), 179, 9)

@Then("the search field is empty")
def step(context):
    test.compare(waitForObjectExists(names.o_UISearchBarTextField).text, "")
Given("elements application is running", function(context) {
    startApplication("Elements");
});

When("I switch to the search view", function(context) {
    tapObject(waitForObject(names.searchUILabel), 179, 9);
});

Then("the search field is empty", function(context) {
    test.compare(waitForObjectExists(names.uISearchBarTextField).text, "");
});
Given("elements application is running", sub {
    my $context = shift;
    startApplication("Elements");
});

When("I switch to the search view", sub {
    my $context = shift;
    tapObject(waitForObject($Names::search_uilabel), 179, 9);
});

Then("the search field is empty", sub {
    my $context = shift;
    test::compare(waitForObjectExists($Names::uisearchbartextfield)->text, "");
});
Given("elements application is running") do |context|
    startApplication("Elements")
end

When("I switch to the search view") do |context|
    tapObject(waitForObject(Names::Search_UILabel), 179, 9)
end

Then("the search field is empty") do |context|
    Test.compare(waitForObjectExists(Names::UISearchBarTextField).text, "")
end
Given "elements application is running" {context} {
    startApplication "Elements"
}

When "I switch to the search view" {context} {
    invoke tapObject [waitForObject $names::Search_UILabel] 179 9
}

Then "the search field is empty" {context} {
    test compare [property get [waitForObjectExists $names::UISearchBarTextField] text] ""
}

上述示例已在教程中简化。为了充分利用 Squish 中的行为驱动测试,请熟悉 行为驱动测试 部分,该部分可在 API 参考手册 中找到。

©2024 The Qt Company Ltd. 本文档中的贡献归其各自的拥有者享有著作权。
提供的文档是根据 Free Software Foundation 发布的 GNU自由文档许可协议版本1.3 的条款许可的。
Qt 和相应的标志是芬兰和/或其他国家/地区的 The Qt Company Ltd. 的商标。所有其他商标均为其各自拥有者的财产。