Robust locator strategy: custom attributes for test automation

Donatas Uznys
6 min readOct 30, 2019

--

I think custom attributes for test automation is a locator strategy that is not getting enough exposure. When I had to present it someone who was not aware of it, I couldn’t find articles, conference presentations or books on it. What I had found was two sentences at most. Even then it’s considered nice-to-have instead of an industry standard.

tl;dr: Add custom attributes for test automation directly into your front-end code. This way reduces test brittleness significantly and simplifies test automation framework if you refactor the code accordingly.

Typical web element locator strategy is focused on which of the following is a better locator: id, name, class, tag name, CSS selector or XPath. This is irrelevant to web applications that you have control of.

The first few times the developers added custom attributes into the code for the test automation I was writing. Then I started doing it on my own as I have direct access to the source code.

Let’s say I want to click Submit button on a page using automated tests. I add this attribute to the relevant element:

data-test-id=submit-button

I find the element in the code and add a custom attribute that has no side effects to the application. As long as unit tests are updated, the build will be green and it will be safe to deploy to production.

Shouldn’t I check for existing locators?

No. By adding a custom attribute to the code, you are communicating to the developers and telling them what is important. They might move the custom attribute in case of refactoring. However, no developer would consider removing it. You can stop updating test automation when a test fails.

This is just the start of the strategy that I’m proposing.

Are you using Page Object pattern? If so, you can greatly benefit by refactoring your code based on a simple assumption: 99% of web element locators conform to the custom attribute naming convention of data-test-id=<element-name-in-kebab-case>. The choice of programming language is of no significance for neither the app source nor the automation code.

In case you’re using Java, your typical page object might start with something like this:

@FindBy(css=”[data-test-id=user-name-input]”)
private WebElement userNameInput;

@FindBy(css=”[data-test-id=password-input]”)
private WebElement passwordInput;

@FindBy(css=”[data-test-id=submit-button]”)
private WebElement submitButton;

If you look closely, there’s code duplication. There’s an obvious pattern that can be refactored.

Let’s consider creating a centralized locator provider. The justification will come later.

Locator provider should have a method (or a function, depending on your programming language) that takes element name and returns a locator. Obviously, this method takes a string, formats it into kebab case, adds a prefix and a suffix. Optionally, locator string can be mapped to a specific object, e.g. class By of Selenium WebDriver.

Now you don’t need the declarations of the locator presented in the code snippet above. Remove them and call the locator provider when it’s needed:

LocatorProvider.get(“Submit button”)

Let’s remove the “magic string”.

LocatorProvider.get(ElementNames.SUBMIT_BUTTON)

The result is the locator we have defined in the beginning.

We’re halfway there. I assume you don’t see the value yet.

Do you have a Common page in your pile of page objects? I am not referring to an abstract class that app the pages inherit. I am referring to a page that has the most common and probably basic actions: click an element, get text of an element, etc.

In case I want to click the Submit button, Common page would have the implementation for this very basic action. I want to click elements regardless of the page I am on.

commonPage.clickElement(elementName)

Here’s the basic implementation of clickElement method using Selenium WebDriver in Java:

driver.findElement(LocatorProvider.get(elementName)).click()

The code looks clean. There’s no need to bloat the code with declarations for every single locator. I can use this page in all of my tests.

Let’s take another leap of faith. Let’s clean up the rest of the page objects.

The rest of the page objects could use Common page methods instead of duplicating the code. A Login page needs to input some text and click a button. That sounds a lot like the actions you’d expect to find in Common page. The Login page method for filling in the user credentials could look like this:

commonPage.inputText(userName, ElementNames.USER_NAME_INPUT)
commonPage.inputText(password, ElementNames.PASSWORD_INPUT)
commonPage.click(ElementNames.SUBMIT_BUTTON)

I don’t need any explicit declarations of locators here.

Let’s discuss the element naming convention next.

I assume there might be multiple Submit buttons in the same page. How do you differentiate them? I suggest avoiding long descriptive element names here. Instead of “Email preferences Submit button” and “User profile Submit button”, you can split it into “Submit button” in “Email preferences section” and “Submit button” in “User profile section”. Then the CSS selectors are:

[data-test-id=email-preferences-section] [data-test-id=submit-button]
[data-test-id=user-profile-section] [data-test-id=submit-button]

This is easier to implement in the front-end code and I find it easier to read in the DOM, especially for tables. Also this improves element naming consistency: when a page, section or some other group has a parent element with custom attribute, everyone will reuse it instead of constructing long element names the way they like.

I don’t have to inspect DOM for locators all that often.

This element locator chaining can be implemented by overloading the locator provider method we’re already using:

LocatorProvider.get(ElementNames.EMAIL_PREFERENCES_SECTION, ElementNames.SUBMIT_BUTTON)

First argument is the parent element name and the second one is the child name. In CSS you chain them with a space character. In the locator provider you have to get CSS selector string for each of the elements separately and then join them with a space character in-between.

Let’s add the last missing piece in this approach — the non-generic element locators. The ones that don’t match the locator convention.

Sometimes adding custom attributes is impossible. Typically this happens with third-party components like embedded Google Maps. Sometimes you need to match CSS properties, e.g. locate checked radio button. This requires additional functionality in locator provider and no changes anywhere else.

Locator provider needs two private methods: one that returns generic CSS selector (we already have that) and another that returns non-generic CSS selector. The public method should look for non-generic selector first and fallback to generic selector in case there is no match.

The missing method should have a map with the problematic CSS selectors. The key should be element name and the value should be CSS selector. For example:

{
ElementNames.CHECKED_CHECKBOX, “[data-test-id=checkbox]:checked”;
ElementNames.UNCHECKED_CHECKBOX, “[data-test-id=checkbox]:not(:checked)”;
ElementNames.NEXT_MONTH_BUTTON, “[display=visible] [data-test-id=next-month-button]”;
}

In my practice the list of non-generic selectors is very short.

Automated end-to-end tests are a lot more brittle than other types of automated tests. There’s a lot that can go wrong. These tests are relatively slow and that’s the reason why it hurts so much to get false negative results.

Locator brittleness is a risk that can be removed by taking full control of the element locators. Locator brittleness is an issue that plagues inexperienced test automation engineers the most. With use of custom attributes for test automation, you don’t need to study CSS selectors and XPath. Also you avoid the issue of long selector strings without any effort.

Centralized locator provider approach works great with the custom attributes. I think it’s a bad idea otherwise. And I’ve tried the approach with JavaScript and Java test frameworks.

I write Gherkin feature files and that keeps the element name list in ElementNames very short. Look at this example:

Given I am in “Home page”
When I click “User icon”
Then I should see “User name” in “Page header”

When you’re using the functionality of Common page object, you don’t have to declare the element names or the locators in the test framework. You add the four custom attributes to the application source code if they are missing and that’s all. Custom attributes enable greater reuse of Common page object.

--

--