Improved Management Of Object Names

Improved Management Of Object Names

TL;DR: Using plain script language variables over the standard objects.map file simplifies refactoring and maintenance at the expense of losing the ability to generate (or reuse) the names when recording.

NOTE: Throughout this blog article, we will use the JavaScript language for code samples. Everything shown here can be expressed in any of the programming languages shipped by Squish though, usually with only minor syntactic changes.

Object Names

To reference controls in a user interface, Squish uses so-called ‘multi property names’ in test scripts. Such names are basically strings using a dictionary-like notation to express a set of constraints which any matching object has to satisfy. For instance, the name

{type='Button' text='OK'}

matches the first found object for which the type property has the value Button and the text property has the value OK. Object names can also be nested to express relationships between objects. For instance, the name

{container={type'Dialog' text='New Document'} type='Button' text='OK'}

would match the first object with the type property Button and the text property OK which is contained (hence the container constraint) in an object whose type is Dialog and the text is New Document. This naming scheme has proven to be very powerful and is successfully used by Squish customers. It provides a number of useful benefits:

  • The naming scheme is entirely independent on the visual rendering of the control, i.e. the screen resolution or X/Y coordinates do not matter.
  • It’s also possible to use wildcards (or even regular expressions) in object names.
  • The object names are generated automatically as tests are recorded.
  • The set of properties used for generated object names is highly configurable to satisfy the automation requirements on a vast range of applications (e.g. translated user interfaces).

Using this naming scheme which is both expressive but also robust is an important tool to ensure that test scripts are easy to maintain even if the application changes (e.g. buttons get reordered). Hence, it is no surprised that object names are ubiquitous in Squish test scripts.

Duplication Impedes Maintainability

However, the widespread use of object names also tends to introduce some problems. Consider an object name which might be used to click on the File menu item in a menu bar, e.g.

{container={type='MenuBar'} type='MenuItem' text='File'}

It’s not hard to imagine that this object name is used in various of the test script code, e.g. when creating a new file, or when opening an existing file, or when exiting the application via File->Quit. So there are multiple places with script code like

mouseClick(waitForObject("{container={type='MenuBar'} type='MenuItem' text='File'}"));

The risk imposed by this duplication of the object name in multiple places manifests as a problem in case you need to change the object name. For instance, it may be that the requirement is brought up that the tests should still run even if the application under test is executed with a different language such that all text labels in the user interface are shown in e.g. German. In that case, the text property of the menu item would no longer be called File but rather Datei. Instead, you could use some internal identifier (maybe called itemId or such) which has a value which is not dependant on the user interface language.

Since the object name of the menu item is repeated in multiple locations, it’s necessary to update all occurrences of the name to not use the text property anymore. This includes the risk of missing some occurrence of the object name.

Object Maps Avoid Duplication

Squish mitigates this issue by including the notion of an Object Map. The idea is that instead of using actual object names in the script code, a free-form ‘symbolic name’ is used instead. When executing the test, Squish translates the ‘symbolic name’ into an actual object name by performing a lookup in the object map, which is a simple table of two columns mapping each symbolic name to an object name.

By default, the object map is implemented as a plain tab-separated text file called objects.map. This file is stored in the test suite directory (suite_…). Symbolic names in this object map are free-form strings with the only requirement that they start with a colon (‘:’). Hence, in the script code you typically see statements like

mouseClick(waitForObject(":File_MenuItem"));

And the objects.map file then contains

:File_MenuItem	{container={type='MenuBar'} type='MenuItem' text='File'}

This implementation is elegant both in it’s functionality as well as it’s simplicity:

  • The functionality of the objects.map file is easy to understand, there is no ‘hidden magic’.
  • The file is stored as a plain text file which makes it viable to store it in revision control systems (Subversion, Git etc.) and view textual differences between two version of the file.
  • The file format is simple enough to process the file with separate (possibly hand-written) tools, i.e. it’s easy to generate the objects.map file automatically or parse it for further analysis.

Squish also provides an Object Map API to access the text file programmatically.

By avoiding the need to duplicate object names, the objects.map file is very effective at simplifying test code maintenance.

Downsides Of The objects.map file

Alas, the way the object map is implemented in Squish also lacks a couple of desirable features features:

  • Detecting stale (i.e. unused) symbolic names is hard, so the objects.map file tends to grow over time.
  • Renaming symbolic names is difficult since locating all occurrences of the symbolic name can be difficult, especially if one symbolic name is a prefix of another (consider the two names :myTree and :myTreeItem – searching for the former text will also yield hits for the latter).
  • It’s not easily possible to document symbolic names; occasionally, it is hard to come up with short but descriptive symbolic names, so many symbolic names are either overly long (obscuring the test code) or too short (obscuring the purpose of the object name).
  • Multiple similiar symbolic names tend to suggest that you can ‘construct’ them at runtime to match arbitrary objects. E.g. given an object map like
    :File_MenuItem	{container={type='MenuBar'} type='MenuItem' text='File'}
    :Edit_MenuItem	{container={type='MenuBar'} type='MenuItem' text='Edit'}
    

    and script code which interacts with these two menu items, e.g.

    mouseClick(waitForObject(":File_MenuItem"));
    // ...
    mouseClick(waitForObject(":Edit_MenuItem"));
    

    it’s not hard to imagine that a user may think that these symbolic names can be constructed at will to match random menu items, e.g. as in

    var menuItemText = "Help";
    if (something) {
      menuItemText = "Window";
    }
    
    mouseClick(waitForObject(":" + menuItemText + "_MenuItem"));
    

    However, this will just cause an error when executing the test, since the referenced symbolic name has to exist in the objects.map file, which may be true when the code was written, but might not be true anymore in the future.

Such features are particularly important to users who do not want to rely on recording scripts alone but rather write and maintain test code manually, an approach to automated testing which we at froglogic very much endorse. Hence, an alternative definition of the object map concept might be desirable.

A Primitive Object Map

At this point, it’s useful to reconsider the concept which the object map captures: it maps symbolic identifiers to object names. Object names are plain strings. As it happens, many programming languages – certainly the ones which are available in Squish – have solved this problem a long time ago. A symbolic name for a string is typically called a constant, or a plain variable! It is not hard to see that an objects.map entry such as

:File_MenuItem	{container={type='MenuBar'} type='MenuItem' text='File'}

which is then used in the test script like

mouseClick(waitForObject(":File_MenuItem"));

is equivalent to a plain variable in a file called e.g. objectsmap.js:

fileMenuItem		= "{container={type='MenuBar'} type='MenuItem' text='File'}"

which is then used like

mouseClick(waitForObject(fileMenuItem));

Our custom, script-based object map, could then be stored in the shared/scripts directory of the test suite and then included in all test cases via the source() function provided by Squish:

source(findFile('scripts', 'objectmap.js'));

Primitive Is Good

The script-based object map seems like a small step, but immediately enables many of the desired features outlined above:

  • Detecting stale (i.e. unused) symbolic names is now very easy: in the Squish IDE, simply right-click any of the variables in the object map file and have the IDE show all the references for that symbolic name, if any. If there is no code referencing that variable, you can safely remove it.
  • The IDE supported right-clicking symbolic names to jump to the corresponding objects.map entry. This is still possible, by selecting Go to Definition in the IDE with the text cursor on a variable.
  • It’s very easy to rename symbolic names: the IDE provides refactoring tools to rename a variable and update all uses of that variable, too.
  • It’s straightforward to document symbolic names: simply use the language-specific documentation notation for the variables (e.g. JavaDoc for JavaScript).

There is however a downside though: it is not possible to record these object names. The IDE does not (yet?) see how to generate the object map script code or which names are already defined (which is important for deciding whether to generate a new name or rather reuse an existing one). For advanced users who do not need the recording functionality very much anyway though, this is not much of a problem and hence a script-based object map can provide a lot of benefit.

In the next blog entry, we’ll take this idea a step further and talk about parametrised object names, composing object names as well as convenience functions for constructing object names “out of thin air”.