Object Map

This section introduces the concept of an automatic object map (often called a GUI object map in QA literature), and how Squish implements this concept.

The Concept of the Object Map

The object map is designed to make it easier to maintain test scripts when the application under test changes its object hierarchy or its object names.

As the set of test cases gets larger, there will be more and more places in the test script code at which GUI controls are referenced. In particular, there may well be multiple places in which the same controls are referenced. For example, it's not hard to imagine that many of the test cases involve selecting the File > Exit menu item to quit the application. This means that there is some duplication of information in the test script code: the object name which identifies the menu item is mentioned multiple times, and it's this duplication which can negatively impact the maintainability of the test code in the future (for this reason, the duplication of information is sometimes referred to as a technical debt).

The risk associated with duplicating object names manifests as a problem once the AUT changes. Revisiting the earlier example, it is imaginable that a review of the user interface design results in the decision to rename the Exit menu item to Quit. Updating the AUT accordingly will suddenly make all test cases expecting a menu item labeled Exit to exist fail to replay since there is no such menu item of course. Hence, all test cases referencing the Exit need to be updated to now select the Quit menu item.

Updating the test scripts when the AUT changes is not very desirable for multiple reasons, including:

  • Depending on the number of test cases which need to be adjusted, updating the test scripts can take a significant amount of time. This is an additional cost which needs to be considered when estimating how expensive a change is to the AUT.
  • Updating the test cases is error prone since failing to update all occurrences of an object name will only become visible when actually executing the test.
  • Since both the AUT as well as the tests themselves are changed, greater care must be taken when comparing the test results. Comparing test results is a lot easier and safer when it's known that only the AUT changed between two test runs, but the tests themselves stayed the same since this ensures that. This approach is in the spirit of the OFAT method of designing experiments.

However, if the AUT changes as in the above example, clearly some change is needed. We would like to minimise the required change though and this is precisely where the Object Map concept comes into play.

Instead of repeating object names in multiple spots of the test script code base, the idea of the Object Map is to maintain a repository of all object names in which each object name is defined exactly once, centrally. Test script code does not use the object names directly anymore. Instead, the Object Map associated each object name with a so-called symbolic name, a free-form identifier which serves as a 'key' into the Object Map. Test scripts exclusively reference object names by using symbolic names, and the symbolic names are automatically mapped to the referenced object names as a test case is executed.

The benefit of this is that changes to the application (such as renaming Exit to Quit) only require changing the single Object Map entry mentioning that object name. The symbolic names are independent of the AUT and thus don't need any modification. Hence, all test scripts can remain unchanged and the test cases replay as before since each symbolic name mentioned in the test scripts automatically maps to the new & updated object name.

There are multiple ways to implement the Object Map concept; two approaches for which Squish provides built-in support are Text-Based Object Map and Script-Based Object Map. The details of each implementation are discussed further down, for the most part their usage is the same though, as discussed in the following sections.

Creating an Object Map

Whenever Squish records a test case, it creates an Object Map for the suite if one does not already exist, or it uses the existing one—and in either case it adds a symbolic name–real name pair for every object that is accessed in the course of the recording, unless that pair is already in the map (for example, due to being added in a previous recording or earlier in the current recording).

However, it is also possible to create an Object Map by hand by manually adding entries to it. Since Object Maps are stored as plain text (using the UTF-8 encoding), so it is possible to edit them using a plain text editor. However, we recommend editing the Object Map through the Squish IDE. The Squish IDE's Object Map editor will ensure that the Object Map is not accidentally corrupted (as might happen when hand editing), and also provides some very convenient features that make editing easier—in particular an area for adding and editing object properties.

Editing an Object Map

If you are starting from scratch and want to write tests by hand and to use symbolic names, there are several approaches you can take:

  • In many cases the easiest and quickest approach is to record a dummy test where you make sure that you interact with every object that you plan to use in your hand written tests. During the recording Squish will populate the Object Map, so when it comes to writing your tests you can simply copy and paste the symbolic (or real) names that you need as you need them.
  • Alternatively, you could just Spy the application's window—this will cause the Spy to populate its Object tree with all the application's objects, and then you can right-click each object you are interested in and use the context menu that pops up to add the object to the Object Map. (And of course you could use the Spy tool's Object Picker to spy individual objects if you just needed to add a few of them to the Object Map.)
  • Another approach is to edit the Object Map manually, inserting your own symbolic names—you can use any names you like providing the properties you set for each one uniquely identifies the object and matches an actual object in the application. (New Squish users are recommended to use one of the other approaches—once you have seen a few Object Maps in practice you'll be ready to use this approach.)

As noted earlier, if an object's symbolic name becomes invalid (for example, due to a change in its object name or one of its parents' object names), you can edit the Object Map, changing the properties associated with the symbolic name to match those that have changed. You can use the How to Use the Spy tool to find the object in the AUT's user interface and add the object (under a new symbolic name, since some aspect of it has changed), to the Object Map. You can then look up the new symbolic name in the Object Map to find out what its property values are, and change the original symbolic name's properties to match. This way, scripts that use the original symbolic name will continue to run correctly, because Squish will look up the symbolic name in the Object Map and then derive the real name from the (now corrected) properties.

When the Object Map is visible when the Spy is actively spying the application under test, it is possible to check which names in the Object Map are valid. To do this, select one or more symbolic names in the Object Map, then right-click to pop up the context menu and choose the Check Object Existence context menu option to check the selected symbolic names.

It is also possible to edit an object's symbolic name in the Object Map. Beware that if you do this, then you must change every occurrence of this name in every test script that uses the Object Map where you changed the symbolic name, replacing the old name with the new one throughout. If you don't do this, then the scripts that use the old (and now no longer existing) name, will fail.

For information about handling the Object Map programmatically in test scripts see Object Map Functions. For information about manipulating the Object Map using the Squish IDE see the Object Map view.

Script-Based Object Map

Starting with Squish 6.4, script-based object maps are the default way to define and maintain the object map. This approach to managing object names is based on a script file defining variables representing the object names. Existing test suites can be switched to this implementation by clicking Convert to Script in the General section of the Test Suite Settings. The Initial Object Map Style for new test suites can be changed from Preferences Squish > Test Creation.

Storage Location of Script-Based Object Maps

The scripted object map is located in the shared/scripts subdirectory of a test suite. A file called names.ext (where <ext> is the file extension corresponding to the scripting language used in the test suite, e.g. js for JavaScript) is expected to exist in that directory.

Shared and Split Script-Based Object Maps

While the default location and name of the Script-Based Object Map cannot be changed it is possible to include other files from within the Script-Based Object Map which can be used to share a Script-Based Object Map between test suites or to split the Object Map and organize it into multiple files to improve maintainability.

Every script you want to include in your Script-Based Object Map either needs to be in the shared/scripts subdirectory of your test suite or in a global scripts folder if you want to share it between multiple test suites (see Global Scripts view).

For the following examples we will assume there is a localnames.ext script file in the shared/scripts subdirectory of your test suite to demonstrate how to split an Object Map into multiple files and a globalnames.ext script file in a global scripts folder to show how an Object Map can be shared between multiple test suites. While there might be many ways to achieve the same results, the examples show the way that is supported by the Squish IDE to make sure that features like the auto-completion and refactoring still work.

This example shows the content of the names.ext. Simply include the local and global script files at the beginning of the Script-Based Object Map.

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
}

This example shows the content of the globalnames.ext. It references the "itemviews" example application.

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]

}

This example shows the content of the localnames.ext. It also shows how to reference names defined in other script files, which might be necessary when splitting Object Maps.

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]

}

Structure of Script-Based Object Maps

A script-based object map is a standard script file which looks slightly different depending on the scripting language being used for the test suite. Here are some examples for different scripting languages:

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

The basic layout of a script-based object map is that it defines a sequence of variables, each of which corresponding to an object name. The variable name is the symbolic name used throughout the test script and the variable's value is the object name itself. The value of variables which Squish generates automatically (e.g. as part of a recording session) are discrete objects representing key-value pairs describing the properties by which an object is identified.

Every script-based object map is made of the same components:

  1. A separate module objectmaphelper (the name may vary slightly between different scripting languages) is loaded; this module provides access to various utility API such as the Wildcard identifier which is used to denote a wildcard expression value.
  2. A namespace is created, commonly called Names or names. To avoid clashes of the variables representing object names and other script variables, all variables representing object names are defined in a single name space.

    Note: It is not possible to rename this namespace.

  3. A sequence of variable definitions, each of which corresponding to an object map entry.

Script-Based Object Map API

Expressing Object Names in Script Code

Object names are most easily expressed as native script hashes resp. dictionaries which define a set of key-values pairs representing the properties to be used for identifying an object. It's also possible to use plain strings in case e.g. hierarchical object names are used.

Note: When using the Tcl scripting language, a dedicated ObjectName command has to be used which takes a variable number of arguments.

Here are some examples showing how to construct an object name matching a GUI control of type Button which is visible and which has the text OK; the button is contained in a control of type Dialog with the caption Login:

# 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}

Note how in each language, a variable is assigned a set of key-value pairs - each value can be a string, but also a script-native value such as a boolean or an integer value. Furthermore, object names can reference each other as seen with the container property in the above examples.

Using Wildcard and Regular Expression Matching

To perform a wildcard comparison, the objectmaphelper module for JavaScript, Python and Ruby offers a dedicated Wildcard type, the Perl module uses a wildcard subroutine and the Tcl module uses a -wildcard command.

For example, an object name which identifies the GUI control of type MainWindow with a text matching (i.e. starting with) AcmeApp v*, you can use:

# 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*")}

To perform regular expression matching, the objectmaphelper module for JavaScript, Python and Ruby offers a RegularExpression type, the Perl module uses a regexp subroutine and the Tcl module uses a -regularexpression command:

# 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.]+")}

Script-Based Object Map Templates

Every time a new script-based object map is created (e.g. when recording for the first time in a new test suite), Squish will create a new script file representing the object map. The script code written is not built into Squish though but rather taken from a template file which is part of the Squish installation.

The template files are stored in the Squish installation directory, under scriptmodules/language/objectmap_template.ext, with <language> being the script language used for the test suite and <ext> corresponding to the file extension used for script files. For example, the template file used for creating script-based object maps in Python is stored in scriptmodules/python/objectmap_template.py.

Here are the default contents of the templates files in different scripting languages:

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

You are free to modify these template files in order to use a custom default layout, e.g. for including additional commentary, loading external files automatically or importing standard modules.

Note: Any modifications to the object map template files are specific to that particular Squish installation. They will be removed when Squish is uninstalled, and installing a new version of Squish will come with the standard (potentially different) template scripts.

Text-Based Object Map

The classic implementation of the Object Map concept is based on plain text files. This implementation can be the default for new test suites, by opening the Preferences dialog and then selecting Squish > Test Creation. From the Test Creation settings page, select Text-Based Object Map.

Storage Location of Text-Based Object Maps

When using a text-based object map, the object map consists of a single file called objects.map which is commonly saved in the test suite's root directory. This file is then shared by all test cases in a test suite.

In some cases it is more convenient to store the Object Map elsewhere, particularly if we want to share it between test suites. This can be configured in the Test Suite Settings dialog, or it can be done by editing the test suite's suite.conf file (which is in the test suite's root directory). Simply add a new key=value pair using the key, OBJECTMAP, and with the value set to the shared object map's path and filename. The path value can be an absolute or relative path. Here is an example that uses an absolute path:

OBJECTMAP = C:\shared\myobjects.map

Note: You can share scripts and test data between test suites, as instructed in How to Create and Use Shared Data and Shared Scripts.

Structure of Text-Based Object Maps

Text-based object maps use plain strings for both symbolic names as well as the referenced real names. Symbolic names are strings starting with a colon (e.g. :Ok_Button) and real names consist of a set of space-separated <propertyName>=<value> pairs enclosed in curly braces. For example, a multi-property name which identifies the object whose type is "Button" and whose text is "Hello" will look like this:

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

The object map itself is a plain text file (using the UTF-8 encoding). Each line in the file corresponds to one entry in the object map. A line consists of a symbolic name and a real name, separated by a tabstop character, e.g.:

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

© 2023 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners.
The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation.
Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.