By Zac Campbell
If you’ve come here looking for the perfect, unbreakable locator, then I’m afraid to tell you that there is no perfect locator. That HTML changes and locators become incompatible are realities of writing automated UI tests. You’ll have to get used to updating them as the development teams experiment with design, streamline HTML and fix bugs as long as your web app is evolving. Maintaining locators must be calculated as part of the cost of test maintenance.
However, the good news is that there is a difference between a good and poorly written locator. That means if you’re smart about your locators you can reduce the cost of maintenance and focus your time on more important tasks than debugging false negative results.
On the other hand, a failing locator is a good thing so don’t be afraid of it. The trusty NoSuchElementException, rather than an assertion failure, is often your first sign that there is a regression in your software.
In this guide I will assume that you know how to write a locator already and are familiar with the construction and syntax of CSS and Xpath locators. From here on we’ll dig into the differences between a good and bad locator in the context of writing Selenium tests.
IDs are king!
IDs are the safest locator option and should always be your first choice. By W3C standards, it should be unique in the page meaning you will never have a problem with finding more than one element matching the locator.
The ID is also independent of the element type and location in the tree and thus if the developer moves the element or changes its type WebDriver can still locate it.
If you have flexible developers or even an eye for the app source code you can always try and get extra IDs added into the code by buying them a beer on Friday evening, taking their sister on a date or just plain begging. However, sometimes adding IDs everywhere is impractical or not viable so we need to use CSS or Xpath locators.
CSS and Xpath locators
CSS and Xpath locators are conceptually very similar so I’ve put them together for this discussion.
These types of locators with combinations of tag name, descendant elements, CSS class or element attribute makes the matching pattern strict or loose, strict meaning that small HTML changes will invalidate it and lose meaning that it might match more than one HTML element.
When writing a CSS or Xpath locator it’s all about finding the balance between strict and loose; durable enough to work with HTML changes and strict enough to fail when the app fails.
Find an anchoring element
A good way to start a CSS or Xpath locator is to start with an element that you know is not likely to change much and use it as an ‘anchor’ in your locator. It may have an ID or stable location but not be the element you need to locate but is a reliable position to search from. Your anchoring element can be above or below the current element in the HTML tree, but most often it’s above.
<div id=”main-section”> <p>Introduction</p> <ul> <li> Option 1</li> </ul> </div>
<div id=”main-section”> <p>Introduction</p> <ul> <li> Option 1</li> </ul> </div> In this example the <li> element that we want to locate does not have an ID or a CSS class making it difficult to locate. There is also the chance there is more than one list in the HTML. The div with the id “main-section” makes a good anchoring element from which to find the <li> element. It narrows down the HTML the locator is searching in.
When to use ‘index’ locators like nth-child() and [x]
nth-child(), first-child,  and such index-type locators should only be used if you are using it against a list itself. In this case the test should explicitly know it wants to pick an item at that index from the list, for example validating the first item of search results. Using an index-type locator to locate an element that is not index-placed is likely to cause you problems when the order of the elements is changed and thus should be avoided!
<menu> <button>Option 1</button> <button>Option 2</button> <button>Option 3</button> </menu>
//menu/button is a suitable locator only when you know you want to interact with the first menu item regardless of how the list of buttons is sorted. The order of the buttons may change and cause your test to fail. Would this be a legitimate failure or one that requires you to re-write the locator?
Depending upon the objective of your test a non-index based locator like //menu/*[text()=’Option 1’] might be more suitable. <menu> is the ideal anchoring element.
CSS class names often tell their purpose
Front end designers are actually humans too and will often give CSS classes names that represent their purpose.
We can take advantage of this and choose locators that are dependent upon the functionality rather than the styling because styling often changes.
<footer class="form-footer buttons"> <div class="column-1"> <a class="alt button cancel" href="#">Cancel</a> </div> <div class="column-2"> <a class="alt button ok" href="#">Accept</a> </div> </footer>
In this example ignore the class “column-1” and “column-2”. They refer to the layout and thus might be susceptible to changes if the development team decide to adjust the design. It will be more reliable to target the button directly. Although “button.ok” would be quite a ‘loose’ locator there could be more than one on the page. You can use the footer as your anchoring element, making “footer .ok” a good locator in this example.
Spotting future fragility
By observing the HTML you can spot potential future fragility. I’ve intentionally left 3 more things out of the locator in the previous example: the <a>’s tag name, the <a>’s content text and any direct descendents ( > ) between footer and a. In the HTML it looks like the dev team have already changed the text label and the tag from “ok” and “button” respectively. The class, text content and tag names are all mismatched!
If the dev team are indecisive or experimenting with UX and performance improvements, these might still change again so we will err on a slightly “looser” locator that will tolerate some changes in the HTML.
CSS example: div > div > ul > li > span
Xpath example: //div/div/ul/li/span
A direct descendent refers to the parent to child relationship of HTML elements. A good example is the first <li> element inside a <ul>.
A long chain of direct descendents like in the locator example above might help you find an element where there are no classes or IDs but it is sure to be unreliable in the long term. A large block of content without IDs or classes is likely to be very dynamic too and probably move around and change HTML structure often. It only takes one element in the chain to change for your locator to come tumbling down.
If you absolutely must use direct descendents in your locators then try to only use a maximum of one in each locator.
Adjust it for purpose
<section id=”snippet”> <div>Blurb</div> </section>
Only use as much of a locator you need. Less is more! If you are only capturing text then using a locator like “#snippet div” is unnecessary. WebDriver will return the same text content for the locator ‘#snippet and ‘#snippet > div’ but the latter locator would break if the div element were changed to a <p> or <span>.
Locating on element attributes
Locating on attributes is a lot like locating by CSS class – the attribute can be unique but also it can be re-used across many items so tread carefully. You’ll have to decide on a case by case basis.
Generally, it is best to avoid using attributes and focus on tags, CSS classes and IDs but modern HTML5 “data-” attributes are stable attributes to use because they are tightly integrated with the functionality of the web application.
Tag name, link text, name locating strategies
These locating strategies are just shortcuts to locating by attribute or by text string (Xpath: text()). The rules for using these apply to tag name, link text and name too.
In summary: when you are composing a locator look for an ID first and failing that, the next nearest element with an ID (to use as an anchoring element). From there, look at descendents and element attributes to narrow down to the element you want to locate.
Understand exactly the purpose of the locator – is it simply to navigate through the site or is it asserting the order of an element? Should the locator be able to cope if the element moves or should the test fail?
The purpose of the locator will decide how strict, loose and which techniques you will need to use in the locator.
Good luck, and code wisely to save yourself future test maintenance and false negative hassles!