对象映射

本节介绍了自动对象映射的概念(在QA文献中通常称为GUI对象映射),以及Squish如何实现这一概念。

注意:如果您需要视频指导,可以在Qt学院(Qt学院)找到关于Squish对象识别的90分钟在线课程

对象映射的概念

对象映射旨在使维护测试脚本更加容易,当测试的应用程序更改其对象层次结构或对象名称时。

随着测试用例集变得越来越大,在测试脚本代码中将会有越来越多的位置引用GUI控件。特别是,可能会有多个引用相同控件的位置。例如,想象许多测试用例都涉及到选择“文件”>“退出”菜单项来退出应用程序是不难的。这意味着测试脚本代码中存在一些信息重复:用于识别菜单项的对象名称被提及多次,而这种重复会对未来测试代码的维护性产生负面影响(因此,这种信息的重复有时被称为技术债务)。

对象名称重复的风险反映为当AUT更改时出现的问题。回到 earlier 的例子,可以想象审查用户界面设计的结果会导致决定将“退出”菜单项重命名为“退出”。相应地更新AUT后,所有期望存在标记为“退出”的菜单项的测试用例都会失败重播,因为当然没有这样的菜单项。因此,所有引用“退出”的测试用例都需要更新为现在选择“退出”菜单项。

当AUT更改时更新测试脚本不是非常理想,原因有很多,包括

  • 取决于需要调整的测试用例数量,更新测试脚本可能需要大量的时间。这是在估计更改AUT的成本时需要考虑的额外成本。
  • 更新测试用例容易出错,因为未能更新对象名称的所有出现将只在实际执行测试时变得可见。
  • 由于AUT以及测试本身都发生了变化,因此在比较测试结果时必须更加小心。如果已知两次测试运行之间只更改了AUT,而测试本身保持不变,则比较测试结果会更容易且更安全。这种方法符合OFAT方法的精神。

然而,如果AUT发生上述示例中的变化,显然需要对某些内容进行更改。我们希望尽量减少所需的更改,这正是对象映射概念发挥作用的地方。

而不是在测试脚本代码库的多个位置重复对象名称,对象映射的想法是维护一个所有对象名称的存储库,其中每个对象名称只定义一次,中央化。测试脚本代码不再直接使用对象名称。相反,对象映射将每个对象名称与所谓的符号名称相关联,这是一个自由形式的标识符,用作进入对象映射的“键”。测试脚本仅通过使用符号名称引用对象名称,符号名称会在执行测试用例时自动映射到引用的对象名称。

这种方法的优点是,对应用程序的更改(例如,将退出重命名为退出)仅需更改提及该对象名称的单个对象映射条目。这些符号名称与AUT无关,因此不需要任何修改。因此,所有测试脚本都可以保持不变,测试用例可以像以前一样重放,因为测试脚本中提到的每个符号名称都会自动映射到新的和更新的对象名称。

有多种实现对象映射概念的方法;Squish提供内置支持的两种方法是基于文本的对象映射基于脚本的对象映射。以下将详细讨论每种实现方法,尽管它们的用法基本相同,如以下几节所述。

创建对象映射

每当Squish记录测试用例时,如果没有现有对象映射,它将为此测试套件创建一个对象映射,或者使用现有的对象映射——在任何情况下,它都会为在记录过程中访问的每个对象添加一个符号名称-真实名称对,除非该对已经在映射中(例如,由于在之前的记录或当前记录的早期添加)。

然而,也可以手动通过手动添加条目来创建对象映射。由于对象映射以纯文本文档形式存储(使用UTF-8编码),因此可以使用纯文本文档编辑器对其进行编辑。但是,我们建议通过squishide编辑对象映射。squishide的对象映射编辑器将确保对象映射不会意外损坏(当手动编辑时可能会发生这种情况),并且还提供了一些非常方便的特性,这使得编辑更容易——特别是添加和编辑对象属性的区域。

编辑对象映射

如果您从头开始编写测试并希望使用符号名称,您可以采取以下几种方法

  • 在许多情况下,最简单、最快的方法是记录一个虚拟测试用例,确保您与您计划在手动编写的测试中使用的每个对象进行交互。在记录过程中,Squish将填充对象映射,因此当您编写测试时,您只需按需复制并粘贴所需的符号(或真实)名称即可。
  • 或者,您可以仅监听应用程序的窗口——这将使监听器填充其对象树中的所有应用程序对象,然后您可以右键点击您感兴趣的每个对象,并使用弹出的上下文菜单将该对象添加到对象映射。(当然,如果您只是需要将少量对象添加到对象映射中,您也可以使用Spy工具的对象选择器来监视单个对象。)
  • 另一种方法是手动编辑对象映射,插入自己的符号名称——您可以使用您喜欢的任何名称,只要您为每个对象设置的属性具有唯一性,并且与应用程序中的实际对象匹配即可。(建议新Squish用户使用其他方法——一旦您看到几个实际对象映射,您就可以使用这种方法。)

如前所述,如果对象的符号名称变得无效(例如,由于对象名称或其父对象名称发生更改),您可以编辑对象映射,将符号名称关联的属性更改为匹配发生变化的属性。您可以使用如何使用间谍工具,在挂起的用户界面中找到该对象(由于某些方面已更改,因此使用新的符号名称),并将其添加到对象映射中。然后,您可以在对象映射中查找新的符号名称,以了解其属性值,并将原始符号名称的属性更改为匹配。这样,使用原始符号名称的脚本将继续正常运行,因为Squish将在对象映射中查找符号名称,然后从(现在已修正的)属性中导出真实名称。

当间谍在积极地监视测试中的应用程序时,对象映射可见,您可能可以检查对象映射中的哪些名称是有效的。为此,请选择对象映射中的一个或多个符号名称,然后右击以弹出上下文菜单并选择检查对象存在上下文菜单选项来检查选定的符号名称。

您还可以在对象映射中编辑对象的符号名称。请注意,如果您这样做,则必须更改测试脚本中您更改符号名称的所有该名称出现,将旧名称替换为新名称。如果不这样做,则使用旧(现在不再存在)名称的脚本将失败。

有关在测试脚本中通过编程方式处理对象映射的信息,请参阅对象映射函数。有关使用squishide操作对象映射的信息,请参阅对象映射查看器

基于脚本的对象映射

从Squish 6.4版本开始,基于脚本的映射是定义和维护映射的默认方式。这种方法管理对象名称的基础是一个定义对象名称变量的脚本文件。现有的测试套件可以通过在测试套件设置常规部分中单击转换为脚本切换到这种实现。可以通过在Squish > 测试创建首选项中更改为新测试套件设置初始对象映射样式

基于脚本的映射存储位置

脚本化的对象映射位于测试套件的shared/scripts子目录中。在该目录中应存在一个名为names.ext(其中 <ext> 是与测试套件中使用的脚本语言对应的文件扩展名,例如,对于JavaScript为js)的文件。

共享和拆分基于脚本的映射

虽然默认的定位和名称的基于脚本的映射无法更改,但它包括在基于脚本的映射内部的其它文件,这可以用来在测试套件之间共享一个基于脚本的映射,或者将映射分割并组织到多个文件中,以提高可维护性。

您想要包含到您的基于脚本的映射中的每个脚本都应位于您的测试套件的shared/scripts子目录中,如果您想要在多个测试套件之间共享它,则可以在全局脚本文件夹中(请参阅全局脚本视图)。

以下示例中,我们将假设在测试套件的 shared/scripts 子目录下有一个 localnames.ext 脚本文件,以演示如何将对象图拆分为多个文件,以及在全局脚本文件夹中有一个 globalnames.ext 脚本文件,以展示对象图如何在不同测试套件之间共享。虽然可能有许多实现相同结果的方法,但以下示例展示了 squishide 支持的方法,以确保自动完成和重构等特性仍然正常工作。

此示例演示了 names.ext 的内容。简单地将局部和全局脚本文件包含在基于脚本的对象图的开始部分。

import { RegularExpression, Wildcard } from 'objectmaphelper.js';

export * from 'globalnames.js';
export * from 'localnames.js';

//you could define additional names here
# encoding: UTF-8

from objectmaphelper import *

from globalnames import *
from localnames import *

#you could define additional names here
package Names;

use utf8;
use strict;
use warnings;
use Squish::ObjectMapHelper::ObjectName;

require 'globalnames.pl';
require 'localnames.pl';

#you could define additional names here

1;
# encoding: UTF-8

require 'squish/objectmaphelper'

load 'globalnames.rb'
load 'localnames.rb'

module Names

include Squish::ObjectMapHelper

#you could define additional names here

end
package require squish::objectmaphelper

source [findFile "scripts" "globalnames.tcl"]
source [findFile "scripts" "localnames.tcl"]

namespace eval ::names {
    #you could define additional names here
}

此示例演示了 globalnames.ext 的内容。它引用了 "itemviews" 示例应用。

import { RegularExpression, Wildcard } from 'objectmaphelper.js';

export var itemViewsMainWindow = {"type": "MainWindow", "unnamed": 1, "visible": 1, "windowTitle": "Item Views"};
export var itemViewsQListView = {"occurrence": 2, "type": "QListView", "unnamed": 1, "visible": 1, "window": itemViewsMainWindow};
export var theComedyOfErrorsQModelIndex = {"container": itemViewsQListView, "text": "The Comedy of Errors", "type": "QModelIndex"};
# encoding: UTF-8

from objectmaphelper import *

item_Views_MainWindow = {"type": "MainWindow", "unnamed": 1, "visible": 1, "windowTitle": "Item Views"}
item_Views_QListView = {"occurrence": 2, "type": "QListView", "unnamed": 1, "visible": 1, "window": item_Views_MainWindow}
the_Comedy_of_Errors_QModelIndex = {"container": item_Views_QListView, "text": "The Comedy of Errors", "type": "QModelIndex"}
package Names;

use utf8;
use strict;
use warnings;
use Squish::ObjectMapHelper::ObjectName;

our $item_views_mainwindow = {"type" => "MainWindow", "unnamed" => 1, "visible" => 1, "windowTitle" => "Item Views"};
our $item_views_qlistview = {"occurrence" => 2, "type" => "QListView", "unnamed" => 1, "visible" => 1, "window" => $item_views_mainwindow};
our $the_comedy_of_errors_qmodelindex = {"container" => $item_views_qlistview, "text" => "The Comedy of Errors", "type" => "QModelIndex"};

1;
# encoding: UTF-8

require 'squish/objectmaphelper'

module Names

include Squish::ObjectMapHelper

Item_Views_MainWindow = {:type => "MainWindow", :unnamed => 1, :visible => 1, :windowTitle => "Item Views"}
Item_Views_QListView = {:occurrence => 2, :type => "QListView", :unnamed => 1, :visible => 1, :window => Item_Views_MainWindow}
The_Comedy_of_Errors_QModelIndex = {:container => Item_Views_QListView, :text => "The Comedy of Errors", :type => "QModelIndex"}

end
package require squish::objectmaphelper

namespace eval ::names {

set Item_Views_MainWindow [::Squish::ObjectName type MainWindow unnamed 1 visible 1 windowTitle {Item Views}]
set Item_Views_QListView [::Squish::ObjectName occurrence 2 type QListView unnamed 1 visible 1 window $Item_Views_MainWindow]
set The_Comedy_of_Errors_QModelIndexxxx [::Squish::ObjectName container $Item_Views_QListView text {The Comedy of Errors} type QModelIndex]

}

此示例演示了 localnames.ext 的内容。它还展示了如何在拆分对象图时引用其他脚本文件中定义的名称。

import { RegularExpression, Wildcard } from 'objectmaphelper.js';

import * as globalnames from 'globalnames.js';

export var itemViewsQtSplithandleQSplitterHandle = {"name": "qt_splithandle_", "occurrence": 2, "type": "QSplitterHandle", "visible": 1, "window": globalnames.itemViewsMainWindow};
export var itemViewsQtSplithandleQTableWidget = {"aboveWidget": itemViewsQtSplithandleQSplitterHandle, "type": "QTableWidget", "unnamed": 1, "visible": 1, "window": globalnames.itemViewsMainWindow};
export var qtSplithandle21QModelIndex = {"column": 1, "container": itemViewsQtSplithandleQTableWidget, "row": 2, "type": "QModelIndex"};
# encoding: UTF-8

from objectmaphelper import *

import globalnames

item_Views_qt_splithandle_QSplitterHandle = {"name": "qt_splithandle_", "occurrence": 2, "type": "QSplitterHandle", "visible": 1, "window": globalnames.item_Views_MainWindow}
item_Views_qt_splithandle_QTableWidget = {"aboveWidget": item_Views_qt_splithandle_QSplitterHandle, "type": "QTableWidget", "unnamed": 1, "visible": 1, "window": globalnames.item_Views_MainWindow}
qt_splithandle_2_1_QModelIndex = {"column": 1, "container": item_Views_qt_splithandle_QTableWidget, "row": 2, "type": "QModelIndex"}
package Names;

use utf8;
use strict;
use warnings;
use Squish::ObjectMapHelper::ObjectName;

require 'globalnames.pl';

our $item_views_qt_splithandle_qsplitterhandle = {"name" => "qt_splithandle_", "occurrence" => 2, "type" => "QSplitterHandle", "visible" => 1, "window" => $Names::item_views_mainwindow};
our $item_views_qt_splithandle_qtablewidget = {"aboveWidget" => $item_views_qt_splithandle_qsplitterhandle, "type" => "QTableWidget", "unnamed" => 1, "visible" => 1, "window" => $Names::item_views_mainwindow};
our $qt_splithandle_2_1_qmodelindex = {"column" => 1, "container" => $item_views_qt_splithandle_qtablewidget, "row" => 2, "type" => "QModelIndex"};

1;
# encoding: UTF-8

require 'squish/objectmaphelper'

require 'globalnames'

module Names

include Squish::ObjectMapHelper

Item_Views_qt_splithandle_QSplitterHandle = {:name => "qt_splithandle_", :occurrence => 2, :type => "QSplitterHandle", :visible => 1, :window => Names::Item_Views_MainWindow}
Item_Views_qt_splithandle_QTableWidget = {:aboveWidget => Item_Views_qt_splithandle_QSplitterHandle, :type => "QTableWidget", :unnamed => 1, :visible => 1, :window => Names::Item_Views_MainWindow}
Qt_splithandle_2_1_QModelIndex = {:column => 1, :container => Item_Views_qt_splithandle_QTableWidget, :row => 2, :type => "QModelIndex"}

end
package require squish::objectmaphelper

source [findFile "scripts" "globalnames.tcl"]

namespace eval ::names {

set Item_Views_qt_splithandle_QSplitterHandle [::Squish::ObjectName name qt_splithandle_ occurrence 2 type QSplitterHandle visible 1 window $names::Item_Views_MainWindow]
set Item_Views_qt_splithandle_QTableWidget [::Squish::ObjectName aboveWidget $Item_Views_qt_splithandle_QSplitterHandle type QTableWidget unnamed 1 visible 1 window $names::Item_Views_MainWindow]
set qt_splithandle_2_1_QModelIndex [::Squish::ObjectName column 1 container $Item_Views_qt_splithandle_QTableWidget row 2 type QModelIndex]

}

基于脚本的对象图结构

基于脚本的对象图是一个标准脚本文件,它在使用的脚本语言不同时看起来略有不同。以下是一些不同脚本语言的示例。

package require squish::objectmaphelper
# Brings the 'ObjectName' command into scope.

namespace eval ::names {

set Ok_Button [::Squish::ObjectName text Ok type Button]
set Item_Views_MainWindow [::Squish::ObjectName type MainWindow unnamed 1 visible 1 windowTitle {Item Views}]

}
// Include script framework
import { RegularExpression, Wildcard } from 'objectmaphelper.js';

export var okButton = {"text": "Ok", "type": "Button"};
export var itemViewsMainWindow = {"type": "MainWindow", "unnamed": 1, "visible": 1, "windowTitle": "Item Views"};
# encoding: UTF-8

from objectmaphelper import *
# Brings the 'Wildcard' and 'RegularExpression' classes into scope.

ok_Button = {"text": "Ok", "type": "Button"}
item_Views_MainWindow = {"type": "MainWindow", "unnamed": 1, "visible": 1, "windowTitle": "Item Views"}
package Names;

use utf8;
use strict;
use warnings;
use Squish::ObjectMapHelper::ObjectName;

our $ok_button = {"text" => "Ok", "type" => "Button"};
our $item_views_mainwindow = {"type" => "MainWindow", "unnamed" => 1, "visible" => 1, "windowTitle" => "Item Views"};

1;
# encoding: UTF-8
require 'squish/objectmaphelper'

module Names

include Squish::ObjectMapHelper
# Brings the 'Wildcard' and 'RegularExpression' classes into scope.

Ok_Button = {:text => "Ok", :type => "Button"}
Item_Views_MainWindow = {:type => "MainWindow", :unnamed => 1, :visible => 1, :windowTitle => "Item Views"}

end

基于脚本的对象图的基本布局是定义一系列变量,每个变量对应一个对象名。变量名是在整个测试脚本中使用的符号名称,而变量的值本身就是对象名称。Squish自动生成的变量(例如,作为记录会话的一部分)代表了由对象识别的属性描述的键值对。

每个基于脚本的对象图都由相同的组件组成。

  1. 加载一个单独的模块 objectmaphelper(在不同的脚本语言中名称可能略有不同);此模块提供对各种实用API的访问,例如用于表示通配符表达式值的 Wildcard 标识符。
  2. 创建一个命名空间,通常称为 Namesnames。为了防止表示对象名称的变量与脚本中的其他变量冲突,所有表示对象名称的变量都在单个命名空间中定义。

    注意:无法重命名此命名空间。

  3. 变量定义的序列,每个变量对应一个对象图条目。

基于脚本的对象图API

在脚本代码中表达对象名称

对象名称最易于用本地脚本哈希(resp.字典)表示,它定义了一组键值对,表示用于识别对象的属性。如果使用例如层次结构化的对象名称,也可以使用普通字符串。

注意:当使用Tcl脚本语言时,必须使用专用的 ObjectName 命令,该命令接受一个可变数量的参数。

以下是一些示例,演示了如何构建与GUI Button 控件匹配的对象名称,该控件可见且显示文本 OK;该按钮包含在标题为 LoginDialog 控件中。

# Inclusion of 'objectmaphelper' module providing 'ObjectName' command omitted for brevity

set loginDialog [::Squish::ObjectName type Dialog caption Login]
set okButton    [::Squish::ObjectName type Button text OK container $loginDialog visible 1]
var loginDialog = {type: "Dialog", caption: "Login"};
var okButton    = {type: "Button", text: "OK", container: loginDialog, visible: true};
loginDialog = {"type": "Dialog", "caption": "Login"}
okButton    = {"type": "Button", "text": "OK", "container": loginDialog, "visible": True}
$loginDialog = {"type" => "Dialog", "text" => "Login"};
$okButton    = {"type" => "Button", "text" => "OK", "container" => $loginDialog, "visible": 1};
LoginDialog = {:type => "Dialog", :text => "Login"}
OkButton    = {:type => "Button", :text => "OK", :container => LoginDialog, :visible => true}

注意,在每种语言中,变量都分配了一组键值对 - 每个值可以是字符串,也可以是脚本原生值,例如布尔值或整数值。此外,对象名称可以相互引用,如上述示例中的container属性所示。

使用通配符和正则表达式匹配

要执行通配符比较,JavaScript、Python和Ruby的objectmaphelper模块提供了一个专用的Wildcard类型,Perl模块使用wildcard子程序,而Tcl模块使用-wildcard命令。

例如,一个对象名称用于标识类型为MainWindow且文本匹配(即以AcmeApp v*开始的)GUI控件,可以使用以下代码:

# Inclusion of 'objectmaphelper' module omitted for brevity
set mainWindow [::Squish::ObjectName type MainWindow text -wildcard {AcmeApp v*}]
// Inclusion of 'objectmaphelper' module omitted for brevity
var mainWindow = {type: "MainWindow", text: new Wildcard("AcmeApp v*")};
# Inclusion of 'objectmaphelper' module omitted for brevity
mainWindow = {"type": "MainWindow", "text": Wildcard("AcmeApp v*")}
# Inclusion of 'objectmaphelper' module omitted for brevity
$mainWindow = {"type": "MainWindow", "text": wildcard("AcmeApp v*")};
# Inclusion of 'objectmaphelper' module omitted for brevity
MainWindow = {:type => "MainWindow", :text => Wildcard.new("AcmeApp v*")}

要执行正则表达式匹配,JavaScript、Python和Ruby的objectmaphelper模块提供了一个RegularExpression类型,Perl模块使用regexp子程序,Tcl模块使用-regularexpression命令

# Inclusion of 'objectmaphelper' module omitted for brevity
set MainWindow [::Squish::ObjectName new type MainWindow text {-regularexpression {AcmeApp v[0-9.]+}}]
// Inclusion of 'objectmaphelper' module omitted for brevity
export var mainWindow = {"type": "MainWindow", "text": new RegularExpression("AcmeApp v[0-9.]+")};
# Inclusion of 'objectmaphelper' module omitted for brevity
mainWindow = {"type": "MainWindow", "text": RegularExpression("AcmeApp v[0-9.]+")}
# Inclusion of 'objectmaphelper' module omitted for brevity
our $mainwindow = {"type" => "MainWindow", "text" => regularExpression("AcmeApp v[0-9.]+")};
# Inclusion of 'objectmaphelper' module omitted for brevity
MainWindow = {:type => "MainWindow", :text => RegularExpression.new("AcmeApp v[0-9.]+")}

基于脚本的对象映射模板

每次创建新的基于脚本的对象映射时(例如,在新测试套件中首次记录时),Squish都会创建一个新的表示对象映射的脚本文件。尽管脚本代码没有内置到Squish中,但它是从模板文件中获取的,该模板文件是Squish安装的一部分。

模板文件存储在Squish安装目录中,位于scriptmodules/language/objectmap_template.ext,其中<language>是用于测试套件的脚本语言,而<ext>对应于脚本文件的文件扩展名。例如,用于在Python中创建基于脚本的对象映射的模板文件存储在scriptmodules/python/objectmap_template.py中。

以下是不同脚本语言模板文件的默认内容

package require squish::objectmaphelper

namespace eval ::names {
}
import { RegularExpression, Wildcard } from 'objectmaphelper.js';
# encoding: UTF-8

from objectmaphelper import *
package Names;

use utf8;
use strict;
use warnings;
use Squish::ObjectMapHelper::ObjectName;

1;
# encoding: UTF-8

require 'squish/objectmaphelper'

module Names

include Squish::ObjectMapHelper

end

您可以自由修改这些模板文件以使用自定义默认布局,例如,包括额外的注释、自动加载外部文件或导入标准模块。

注意:对对象映射模板文件的任何修改都针对特定的Squish安装。卸载Squish时将删除这些修改,安装Squish的新版本将附带标准(可能不同)的模板脚本。

基于文本的对象映射

对象映射概念的原始实现基于纯文本文件。通过打开首选项对话框,然后选择Squish > 测试创建,可以实现对新测试套件的默认设置。在测试创建设置页面上,选择基于文本的对象映射

基于文本的对象映射的存储位置

在使用基于文本的对象映射时,对象映射由一个名为objects.map的单个文件组成,通常保存在测试套件的根目录中。然后,此文件由测试套件中的所有测试用例共享。

在某些情况下,将对象映射存储在其他地方会更为方便,特别是当我们想在测试套件之间共享它时。这可以在测试套件设置对话框中进行配置,或者可以通过编辑测试套件的suite.conf文件(位于测试套件的根目录中)来实现。只需使用键OBJECTMAP添加一个新的键值对,并将值设置为共享对象映射的路径和文件名。路径值可以是绝对路径或相对路径。以下是一个使用绝对路径的示例:

OBJECTMAP = C:\shared\myobjects.map

注意:您可以根据如何创建和使用共享数据和共享脚本的说明,在测试套件之间共享脚本和测试数据。

文本对象映射的结构

基于文本的对象映射使用普通字符串作为符号名称和引用的实际名称。符号名称以冒号开头(例如::Ok_Button),实际名称由一组用花括号括起来的空格分隔的=配对组成。例如,一个多属性名称,表示类型为"按钮"且文本为"Hello"的对象,其形式如下

{type='Button' text='Hello'}

对象映射本身是一个纯文本文件(使用UTF-8编码)。文件中的每一行都对应于对象映射中的一个条目。一行由一个符号名称和一个实际名称组成,它们由制表符分隔,例如:

:Hello_Button   {type='Button' text='Hello'}

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