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.
IDs are often also used in the web page’s JavaScript so a
developer will avoid changing an element’s ID to avoid having to change
his JavaScript. That’s great for us testers!
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, [1] 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[1] 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.
Direct descendents
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.