In a previous blog entry, we introduced an alternative, script-based, approach to maintaining a mapping of symbolic names (variables) to the actual object names (strings). By using script variables instead of free-form strings, the mapping was no longer stored in a separate text file but instead it was brought into the domain of the programming language, thus enabling a lot of tooling support e.g. for renaming symbolic names or finding references of a symbolic name in the test code.
However, there are still some annoyances related to managing object names like this, most notably caused by the fact that the actual object names are still plain strings:
Object Names As Strings Cause Trouble
- Composing object names is awkward and error-prone. For instance, assume that there are two object names for an imaginary Add User dialog and an OK button in the objectsmap.js file we introduced in the previous blog article:
addUserDialog = "{type='Dialog' text='Add User'}" okButton = "{type='Button' text='OK'}"
Since there can be multiple OK button objects visible at the same time, you might want to introduce a dedicated object name which specifically references the OK button in the Add User dialog by using the special container constraint. You might do:
addUserOKButton = "{container={" + addUserDialog + "} type='Button' text='OK'}"
…i.e. use plain string concatenation. However – notice the mistake we made? The addUserDialog variable has a value which is already enclosed in curly braces, so we don’t need to (in fact: must not) specify them again when using the variable. So what we actually should have done is:
addUserOKButton = "{container=" + addUserDialog + " type='Button' text='OK'}"
- Implementing parametrised object names is fragile. To implement ‘parametrised symbolic names’ (which a programming language usually calls ‘functions’ or ‘procedures’ or ‘subroutines’) you could define a function such as
def dialogName(title): return "{type='Dialog' text='" + title + "'}"
This works well for many invocations such as dialogName(“Add User”) to generate the object name {type=’Dialog’ text=’Add User’}, but there’s a bug in here: what if the title contains a single quote character, e.g. for a dialog which is shown when a file could not be written to disk as in:
cannotWriteFileDialog = dialogName("Can't Write File"); // OOPS: cannotWriteFileDialog is set to {type='Dialog' text='Can't Write File'}
This generates an invalid object name, causing an error at runtime.
Both of these issues are only detected so late (when actually using the object name) because the object names are plain strings. Had we used some more expressive data structure, standard tools such as syntax checkers of ‘lint’-style programs might have detected the issue before we even ran the test.
Object Names Are Dictionaries
Taking a step back to consider what alternatives to plain strings we have for expressing object names, it’s not hard to see that an object name such as
addUserDialog = "{type='Dialog' text='Add User'}"
Is actually a dictionary mapping the key type to the value Dialog and the key text to the value Add User. All scripting languages support by Squish have dedicated support for dealing with dictionaries though! Hence, using e.g. Python as the programming language, we could also say
addUserDialog = {"type": "Dialog", "text": "Add User"}
Notice how the syntax is almost exactly the same. However, since it is now a real Python dictionary, a simple syntax error such as forgetting a ” sign will be highlighted in the Squish IDE right away instead of being noticed when the object name is used.
Composing object names then simply becomes a matter of using the variable (the “symbolic name”) of some object name as the value in another object name dictionary, e.g.:
addUserDialog = {"type": "Dialog", "text": "Add User"} okButton = {"container": addUserDialog, "type": "Button", "text": "OK"}
However, the Squish API such as mouseClick does not work with dictionaries. It requires a plain string (or an object reference). Hence, we need to have a function (called e.g. serialise) to serialise an object name dictionary into a sequence of characters to build a string. Luckily, this is not hard to do. Here’s an initial, incomplete but simple Python definition:
def serialise(name): return "{" + " ".join(k + "='" + str(v) + "'" for k, v in name.iteritems()) + "}"
This function can be invoked with a dictionary and then translates it to the Squish object name syntax. Thus, the following two statements are equivalent:
mouseClick(waitForObject("{type='Dialog' text='Add User'}")); mouseClick(waitForObject(serialise({"type": "Dialog", "text": "Add User"}));
Before we can use the ‘serialise’ function in your code though, we need to take care of three more use cases though.
Nested Object Names
To express relationships between objects (e.g. some object being a container of another), it is desirable to be able to use object name dictionaries as values for other object name dictionaries. For instance, earlier we considered this example:
addUserDialog = {"type": "Dialog", "text": "Add User"} okButton = {"container": addUserDialog, "type": "Button", "text": "OK"}
Right now, a call like serialise(okButton) will not construct the correct name since the definition of the serialise is such that it always gets the native Python str representation of a value, which is not what we want for nested object names:
# This prints {type='Button' container='{'text': 'Add User', 'type': 'Dialog'}' text='OK'} print serialise(okButton) # ...but we wanted {type='Button' container={text='Add User' type='Dialog'} text='OK'}
Instead, we need to make serialise invoke itself recursively. This can be achieved by introducing a little toString helper function:
def serialise(name): def toString(value): if isinstance(value, dict): return serialise(value) else: return "'" + str(value) + "'" return "{" + " ".join(k + "=" + toString(v) for k, v in name.iteritems()) + "}"
Instead of calling str on every vaule of the given object name dictionary, the function now uses a local toString helper function. The helper will cause a recursive call to serialise if the given value is a dictionary, otherwise it invokes str as before and encloses the value in single quotes.
Property Values With Special Characters
As with any syntax, the syntax used for Squish object name implies that certain characters have a special role. Given e.g. the name
{type='Dialog' text='Add User'}
It’s not hard to see that the single quote character (‘) has a special role: it denotes the start and the end of the property value to test. Since it is meant to enclose the property values, and single quotes which might be *part of* the property value need to escape this interpretation. This can be achieved by writing a \ (backslash) character in front of the single quote to be escaped:
Right: {type='Dialog' text='Can\'t Add User'} Wrong: {type='Dialog' text='Can't Add User'}
Consequently, a literal backslash character which should be part of the property value needs to be escaped with a backslash too.
By extending the toString helper function of serialise we can ensure that special characters are always escaped:
def serialise(name): def toString(value): if isinstance(value, dict): return serialise(value) else: stringVal = str(value) stringVal = stringVal.replace("\\", "\\\\") stringVal = stringVal.replace("'", "\\'") return "'" + stringVal + "'" return "{" + " ".join(k + "=" + toString(v) for k, v in name.iteritems()) + "}"
Note how the code (for non-dictionaries) first calls str to get the string representation of the value and then escapes all backslashes and single quotes. Due to the fact that backslashes also have a special role in Python string constants (they serve as the escape character), we need to escape them all resulting in all backslashes to appear two resp. four times.
Wildcard & Regular Expression Matches
As discussed in the Squish manual, Squish supports three different approaches at comparing property values when performing object lookups:
- Exact Matching via ‘=’. This performs a plain string comparison between the actual and the expected property value.
- Wildcard Matching using ‘?=’. This makes object names more flexible by permitting various wildcard characters such as * to be used in the object name which is useful to mask changing texts (e.g. the version number in some main window title).
- Regular Expression Matching via ‘~=’. This is a (much) more powerful but also more complex alternative to wildcard matching. Regular expressions perform expressing a very diverse set of patterns to match.
In order to be able to express that some given dictionary value is not to be used for an exact match, we need a way to ‘tag’ the property values. There are various was to implement this, one way is to use ‘tag’ classes which wrap the plain string values and which can be tested for using the isinstance() function:
class RegExp(object): def __init__(self, value): self.value = value class Wildcard(object): def __init__(self, value): self.value = value
The idea is that client code can use these little wrapper classes like this:
mainWindow = {"type": "Window", "text": Wildcard("Acme App v*")}
The use of the Wildcard class here indicates that the given text Acme App v* is to be treated as a wildcard match, i.e. the * has a special role.
To make our serialise function support this, we can extend the local toString helper function a bit:
def serialise(name): def toString(value): if isinstance(value, dict): return "=" + serialise(value) else: if isinstance(value, RegExp): operator = "~=" stringVal = value.value elif isinstance(value, Wildcard): operator = "?=" stringVal = value.value else: operator = "=" stringVal = str(value) stringVal = stringVal.replace("\\", "\\\\") stringVal = stringVal.replace("'", "\\'") return operator + "'" + stringVal + "'" return "{" + " ".join(k + toString(v) for k, v in name.iteritems()) + "}"
The function now uses a different matching operator depending on the type of the dictionary value passed.
Object Names As Dictionaries To The Rescue
At this point, the serialise is powerful enough to generate any valid Squish object name, but no invalid ones. In particular, it does away with the two issues caused by object names being strings which were raised at the beginning of this blog article:
-
Composing object names is awkward and error-prone.
Composing object names is now as natural as passing variables to a function or using variables as dictionary values: the serialise function ensures that all generated object names are valid, and potential typos caused by writing the name of some object name variable (the “symbolic identifier”) wrongly are caught right away by the IDE or other tools analysing the Python code.
-
Implementing parametrised object names is fragile.
It is now safe to generate object names programmatically because the serialise correctly escapes all special characters. Consider
def dialogName(title): return serialise({"type": "Dialog", "text": title})
This function generates accurate, syntactically valid object names, so you can populate your object map like
loginDialog = dialogName("Login") addUserDialog = dialogName("Add User")
Final Words
Maybe this blog article (and its predecessor) can serve as food for thought on how to improve the maintenance of your test scripts by considering an alternative approach to managing object names. The standard objects.map is a trusty old workhorse, but it’s possible to go a lot further and get a lot more out of Squish with just a little bit of scripting.