Selenium Locators Tutorial: Types, Best Practices, and many more

a darts target symbolizing Selenium locators

The skill of building robust Selenium locators is both science and art. This skill stems from a special kind of intuition that you can only attain while working on real-life projects. Most QAs agree it takes months, sometimes years, to develop this intuition.

So what is it that makes Selenium locators a pain point?

Building locators that last is easy if you’re dealing with elements that have unique IDs. Things are still fairly straightforward if you can add missing IDs every time you need them. But is this a realistic expectation in real-life projects? Sadly, no.

In real life, we always stumble across hard-to-reach stuff. Dynamically generated IDs, duplicate IDs, missing IDs, renamed IDs, pieces of legacy code, you name it. More often than not, you don’t get the luxury of asking a developer to assign an ID to each element you target. In situations of this sort, the efficiency of test automation will depend on your knowledge of Selenium locators and best practices related to them.

Let’s have a quick look at the tactics of targeting UI elements in Selenium tests. In this post, we’ll look at the types of Selenium locators, as well as the advantages they offer and the pitfalls associated with them.

Simple locator tactics in Selenium

Selenium can use CSS or XPath selectors to “find” on-page elements. In addition, you can use convenience methods of the Selenium API for simple cases. The latter include targeting UI elements directly by their HTML tags and attributes like id, class, name, etc. Below, you will find several widespread Selenium locator tactics based on this approach.

Identifier (ID) locators

Static unique ID attributes are the most straightforward and future-proof way to target HTML elements. Here’s a simple example:

<form id="userName" class=”login-form” action="/action_page.php">
  <!-- ... -->
</form>

WebElement userName = driver.findElement(By.id("userName"));

Static ID-based Selenium locators are short, simple, and independent from page structure. They don’t break if you add or remove ancestors, descendants or siblings to the element you’re targeting.

The only two ways to mess up an ID-based Selenium locator is to rename the ID or have duplicate IDs on the same page. It goes without saying that dynamic IDs that change with every page load don’t work the same way as static IDs.

Name locators

Name attributes prove handy when working with forms. Take a look another example:

<form id="userName" action="/action_page.php">
  <input type="text" name="firstName" value="Benedict">
  <input type="text" name="lastName" value="Cumberbatch">
  <input type="submit" value="Submit">
</form>

WebElement firstName = driver.findElement(By.name("firstName"));
WebElement lastName = driver.findElement(By.name("lastName"));

Like IDs, name-based Selenium locators don’t depend on page structure. This means the locators for firstName and lastName won’t break if you relocate the UI elements they target.

On the downside, name attribute values don’t have to be unique. It is not uncommon, for instance, for all radio buttons in a form to share the same name:

<form>
  <input type="radio" name="gender" value="male" checked> Male<br>
  <input type="radio" name="gender" value="female"> Female<br>
  <input type="radio" name="gender" value="other"> Other
</form>

WebElement gender = driver.findElement(By.name("gender"));

In the example above, the gender variable will store the first element with the name gender. Obviously, this locator will break if someone places another element above the one you’re targeting.

Link text and partial link text locators

Link text locators provide a working solution for test cases that cover navigation flows. Check out this navigation menu:

<ul id=”navigation” class="nav">
  <li class="nav__item">
    <a href=https://www.gotspoilers.com/home" class="nav__link">Home</a>
  </li>
  <li class="nav__item">
    <a href="https://www.gotspoilers.com/contact" class="nav__link">Contact Us</a>
  </li>
</ul>

WebElement home = driver.findElement(By.linkText("Home"));

In case you’re not sure about the exact wording of your anchor text, Partial Link Text is an option:

WebElement contact = driver.findElement(By.PartialLinkText("Contact"));

Selenium locators of this kind will only work with links that have anchor text, and they will break if you change that text. Naturally, link text locators are of little use if you have two or more links with the same anchor text.

Tag name locators

Targeting UI elements by their HTML rarely results in a selector with a long lifecycle. Still, doing this might be viable if you’re working with less common HTML tags. For instance, the tactic from the example below will work if you’re 100% sure your page contains one and only one <select> element:

<select>
  <option value="Option1">Option1</option>
  <option value="Option2">Option2</option>
  <option value="Option3">Option3</option>
</select>

List<WebElement> select = driver.findElement(By.tagName("select"));

Class name locators

Class names are more specific than tag names yet less specific than name attributes. The Selenium locator in the example below will target the first (uppermost) menu item with the specified class name and ignore the remaining ones:

<ul id=”navigation” class="nav">
  <li class="nav__item">
   <a href="https://www.gotspoilers.com/home" class="nav__link">Home</a>
  </li>
  <li class="nav__item">
   <a href="https://www.gotspoilers.com/blog" class="nav__link">Blog</a>
  </li>
  <!-- other <li> items with the “nav__item class name” -->
</ul>

WebElement firstNavItem = driver.findElement(By.className(“nav__item”));

CSS and XPath selectors

Non-unique targeting criteria like class names, tag names, name attributes and link text offer little precision or predictability. Whenever you use anything except unique IDs, you can’t be sure that your locator will always target what you think it targets.

To make your locators more precise, you’d want to specify the relations with the elements on the same page using CSS and Xpath selectors. Let’s begin with the former.

CSS selectors

There are over 30 CSS selectors that front-end developers use when styling web pages.

In the example below, the CSS locator targets the first li item in the same navigation menu:

<ul id="navigation" class="nav">
  <li class="nav__item">
    <a href="https://www.gotspoilers.com/home" class="nav__link">Home</a>
  </li> 
  <li class="nav__item">
    <a href="https://www.gotspoilers.com/blog" class="nav__link">Blog</a>
  </li>
</ul>

WebElement firstNavItem = driver.findElement(By.cssSelector("#navigation li:first-child"));

The advantage of this Selenium locator over capturing the nav__item class value is that it uses a unique ID. Should anyone add another element with the nav__item class above the menu, the test won’t break. Adding another first element inside of the menu, on the other hand, will break the test.

XPath selectors

When it comes to working with parent-child structures, XPath offers a powerful alternative to CSS locators in Selenium. This type of locators lets you access UI elements using absolute and relative paths to them. Here’s how you can use absolute and relative XPath when targeting the first navigation menu item from the example above:

Absolute XPath:

String firstNavItem = browser.findElement(By.xpath(“/html/body/ul/li”));

Relative XPath:

String firstNavItem = “//ul[@id=”navigation”]/*[1]”;
browser.findElement(By.xpath(firstNavItem));

Comparing the two, relative XPath is shorter and more specific, which makes it less brittle. Should anyone add a parent element to the <ul>, the absolute selector will cease to function. A relative one will still work.

Accessing element via DOM

Yet another way to access HTML elements is via the Document Object Model, the JavaScript representation of page structure. Targeting the same first item in the navigation menu from the example above may look like this in DOM:

var navItems = document.getElementsByClassName(“nav__item”)[0];

The .getElementsByClassName() method from the example above creates a collection of all elements with the nav__item class. After that, you need to specify what item of the list you’re targeting. In our case, it’s the first item of the navItems list.

It’s easy to see that this locator shares the main weakness of class-based Selenium locators. Should anyone make another item the first one, your text will cease to work as expected.

Selenium locators: best practices

Knowing about locators is one thing, but knowing how to use them is quite another. Being able to build a robust locator begins with understanding of what a robust locator is. Here are three criteria to keep in mind:

  • Robust locators are as simple and small as possible. The more elements a locator contains, the higher the chances it’ll break because of the change in the page structure.
  • Robust locators still work after you change the properties of a UI element. Relying on frequently-changed attributes like modifier classes (menu__item--red) is never a good practice.
  • Robust locators still work after you change the UI elements around the element you target. Whenever you use a non-unique attribute, chances are the locator will break because someone added an element with that same attribute above.

So how do you build reliable locators? Here are several best practices to keep in mind:

Preferred selector order : id > name > css > xpath

IDs are unique, which makes them perfect candidates for robust Selenium locators. Names don’t have to be unique, yet they often are. In any case, duplicate name attributes are less common than duplicate classes. When IDs and unique name attributes aren’t available, though, you’d have to go with more brittle, structure-dependent locators.

Target the nearest ancestor with an ID

If the element you’re targeting doesn’t have a unique ID, chances are its parent or close ancestor has it. If that’s your case, using a CSS selector that includes that element is a good practice, as long as your locator remains fairly short. If you’re targeting a close descendant with a unique ID, XPath should do the job.

When working with dynamic IDs, target the stable part

Static IDs are great, but what if you’re working with a framework that relies on autogenerated IDs? If the dynamic ID has a static part, there’s still hope:

<ul id=“navigation-4815162342” class=“nav”>
  <!-- list items -->
</ul>

WebElement navigation = browser.findElement(By.xpath(“//ul[contains(@id, ‘navigation’)]”));

The XPath selector from this example looks for an ul with an id attribute value that contains the word “navigation”.

How Screenster deals with selectors

No matter how great your Selenium selector is, it is always at risk of breaking because of a new change introduced to the UI. So how do you build selectors that can withstand UI changes?

One way to do this is to write a failover algorithm for the cases when the stored selector gets broken. This nifty algorithm would have to find all possible substitutes for the missing id or class, and then select the best match. It is NOT a simple matter!

The use of a failover algorithm is one of the key features of our very own product Screenster. When scanning a UI, Screenster builds a list of all possible locator criteria for each element. Basically, it generates several selectors to make sure the test won’t break after someone has renamed some ID or class, or moved an element outside of its parent div.

Let’s see how it works with the following example:

<body>
  <div class=”content-wrapper”>
    <div class=”menu-left”>
      <div class=”article full-width”>
        <p>All work and no play makes Jack a dull boy</p>
      </div>
    </div>
  </div>
</body>

WebElement article = driver.findElement(By.className(“article”));

Using a Class Name locator in this example is an option, but the locator will break should anyone add another “article” element above this one. The obvious alternative of a parent-child locator would be just as fragile.

Instead of relying on a single class or parent-child pair, Screenster will automatically generate a list of all selectors for the element. This list includes the tag name and both classes of the targeted element, as well as the tag names and classes of its ancestors and descendants. Should one of the locators get broken, the platform will use a different one:

[
  {"tagName":"HTML"},{"nthOfType":1,"tagName":"BODY"},
  {"classes":["content-wrapper"],"nthOfClass":0,"nthOfType":1,"tagName":"DIV"},
  {"classes":["menu-left"],"nthOfClass":0,"nthOfType":1,"tagName":"DIV"},
  {"classes":["article", “full-width”],"nthOfClass":0,"nthOfType":1,"tagName":"DIV"}
]

The platform builds a tree of DOM ancestors and descendants for each element, and updates these trees whenever someone changes the UI. Basically, it handles all the locator-related work automatically. It certainly saves you a ton of time.

The great thing about this approach is that it can handle cases that involve broken or dynamic IDs:

<body>
  <div id=“header”>
    <ul class=“header__nav nav”>
      <li class="nav__item"><a href=“#”>Home</a></li>
      <li class="nav__item"><a href=“#”>Blog</a></li>
    </ul>
  </div>

  <div class=“content”>
    <div class=“menu-left”>
      <ul class="nav">
        <li class="nav__item"><a href=“#”>Home</a></li>
        <li class="nav__item"><a href=“#”>Article 1</a></li>
        <li class="nav__item"><a href=“#”>Article 2</a></li>
      </ul>
    </div>
  </div>
 </body>

WebElement navItemHome = driver.findElement(By.cssSelector("#header .navigation li:first-child"));

The example above illustrates a complex case. There are two elements with identical class names and link texts, so you need something unique to create robust locator for the first one. The ID of its ancestor (“header”) seems like a viable option. But what if something happens with that ID? With Screenster, this scenario is not a problem.

[
  {"nthOfType":1, "tagName":"BODY"}
  {"id":"header", "nthOfType":1, "tagName":"DIV"},
  {"classes":["header__nav"], "nthOfClass":0, "nthOfType":1, "tagName":"UL"},
  {"classes":["nav__item"], "nthOfClass":0, "nthOfType":1, "tagName":"LI"},
  {"innerHTML":"Home", "nthOfType":1, "tagName":"A"}
]

Should anyone rename or delete the ID, the platform will simply build a new locator. It’ll use the non-duplicate header__nav class name of the li’s parent ul. It’s that simple.

As you see, selectors don’t have to be complicated. In fact, you might even forget about the existence of selectors when using a testing automation platform like Screenster.

Care to learn more?

If the idea of codeless, easy-to-create and robust UI tests sounds exciting, there’s an easy way to find out if Screenster can live up to your expectations. Hit the orange button below this post and try the free version of Screenster online. Try it with your website or web app, and you’ll see that UI testing can be fun. Also, we’d love to hear your feedback!

 

Want to try Screenster on the cloud?

Try Online

 
 

Selenium Locators Tutorial: Types, Best Practices, and many more was last modified: August 15th, 2017 by Ilya Goncharov

WordPress Image Lightbox Plugin