Agnostic Approach of Locators
Solution to rapid changes
Yesterday, I ranted about a scenario where the locators of a Web Application changes from one build to the other. This is a scenario which I have seen in too many workplaces. The solution provided by the Architects in this case has always been a bit on the orthodox. I have seen only the usual way and the over-engineered way.
However, if we as engineers look at the problem statement and think about a solution, there is one solution which I think many would agree on. Create a file which could be referred to at runtime and used in order to generate the classes and load the locators. I am not sure about other programming languages(it should be possible to do so though), but with Python we can come up with a solution as follows.
The schema
First of all we need to design the schema of the file which will be containing all the locator data. As already detailed in the earlier post, we need to make sure that the locating strategy and the locator string is included in the file. This means that there has to be a way to specify the locator strategy, the locator string in such a way that none of the locator strings which could be used to identify elements are being hampered.
There is also another portion, the number of elements to be fetched also needs to be specified. This is something that I learned from the Selenium Page Factory module for Python. The module is good in its approach of locating the element the moment the dot operator is used. So, for example, if the user types somepage.someelement.click(), somepage.someelement will already locate the element and then return a Selenium WebElement reference on which the user would be able to chain the next operation.
The problem with Selenium Page Factory module is that it does not consider this fact that the locator string might require updates and forces the user to update the element locator strings in the locators dictionary. There is also this part where the module only considers each locator string to fetch only a single element. However, there are cases where the user would like to locate multiple elements using a single locator string.
It is at this juncture that we need to engineer our own solution. We will be using some of the concepts of this same selenium_page_factory Python module, however, we will be engineering our own solution. Our approach would be to contain the locating strategy, the locator string and the specification as to whether the locator string would be fetching a single element or multiple elements. We also need to keep in mind the user experience, if we use JSON, the following could be considered as a structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"LoginPage": {
"username": {
"by": "CSS",
"locator": "input[data-test='username-input']",
"multiple": false
},
"password": {
"by": "CSS",
"locator": "input[data-test='password-input']",
"multiple": false
},
"login_button": {
"by": "CSS",
"locator": "input[data-test='submit-button']",
"multiple": false
}
}
}
There is a fundamental problem with this approach. The indentation, despite trying to help the user to read and understand it, causes confusion when more members are to be added. There are also other files which could be leveraged as well, for example TOML, YAML, XML and the like. All of these files suffer from the same basic problem, they have a cognitive complexity to themselves.
Python also uses spaces in order to separate code blocks, however the interpreter actually catches the issues when the program is being run. One could argue that
jsonand similar python modules already report issues in case the file is not having proper format, however, the cognitive complexity is not taken into account for files like YAML and TOML.
If some argue that we should be using YAML file, since it is a “standard”, we would be looking at something as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
LoginPage:
username:
by: CSS
locator: input[data-test='username-input']
multiple: false
password:
by: CSS
locator: input[data-test='password-input']
multiple: false
login_button:
by: CSS
locator: input[data-test='submit-button']
multiple: false
Just take a look at the YAML format above. If someone would have to add on top of the existing data, if they are editing with an editor which does not have auto-indentation present, they might be adding it in another layer of increased and decreased indentation. There is also the part of the expanded tabs as well as spaces in place of tabs.
Not to mention, to find out the issues, one would have to run the code/automation in order for the interpreter to report the errors. So, as we can see, the debugging becomes slow, delayed. That is another portion that is completely unacceptable. The debugging and editing of the locator file(going forward we will be calling the file containing the locator information as the locator file) gets hampered.
The solution is to take a look at an existing format which specifies everything in one single line per element. The best contender in this case is the CFG or INI file. An INI file having the aforementioned information could be written as follows:
1
2
3
4
[LoginPage]
username=CSS | input[data-test='username-input'] | false
password=CSS | input[data-test='password-input'] | false
login_button=CSS | input[data-test='submit-button'] | false
Easy, single line entries, clear enough to be used for specifying whether multiple elements or a single element will be fetched. There is no indentation or bracing or separation of blocks required, so the cognitive complexity also is taken care.
Loading the file
Now that we have selected the locator file format, we will be working on the code that will be responsible for reading this file and loading the elements accordingly. The loading could be done based on the framework/module that is being used in order to write the automation. If someone is using the Behave Python module, one could write the code for loading the elements inside environment.py file.
Irrespective of wherever the code is written, we will concentrate on the task at hand - we will be following the portion of encapsulation and segregation by creating objects at runtime. The Objects would be of a Base class which represents each Page. So, in the aforementioned example, it is the LoginPage. In order to do so, one would have to write a sample base class:
1
2
3
4
5
6
class Page:
def __init__(self, driver):
self.driver = driver # We need this driver later for locating elements
def wait_for(self):
pass # This needs to be implemented in case the user wishes to wait for the page to load.
Now, in order to use this Page, we would be writing something as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from configparser import ConfigParser
from pathlib import Path
...
def load(locator_filepath: str, container: object):
if not len(locator_filepath):
raise ValueError("Locator filepath is not a valid filepath")
if not isinstance(locator_filepath, str):
raise TypeError("Locator filepath should be a non-empty string")
if not Path(locator_filepath).is_file():
raise FileNotFoundError("Locator file is not present in the specified location")
parser = ConfigParser()
parser.optionxform = str
parser.read(locator_filepath)
for section in parser.sections:
tmp = Page(driver=webdriver)
...
for option, value in parser.items(section):
...
setattr(tmp, option, value)
...
setattr(container, section, tmp)
As evident from the snippet above, we can create an instance of the Page class in the sections loop and then set the option and value as attribute name and value respectively. Post that we can set the attribute in the Page instance and then the same instance could be assigned to a container. Here the container could be context from Behave Python module or any class object.
The result of the aforementioned code allows for creating pages at runtime without having to do any “Architect”-ing or Over-engineering at all.
Writing classes upon classes limits the user from freeing themselves from the clutches of Class Complexity and traditional thinking. With respect to how the elements could be located, that is something to be detailed in the next post.