Selectors in Testing: Using Data Attributes to Prolong their Doom

Mar 14, 2022 | By David Rosevear

Dominos

Not again! Another broken selector…

Not another one...

Some front-end code changes were made, and regression failed! All of a sudden tests can't find their elements. Only then to realize it's not the code that broke, but the test selectors themselves no longer work.

Sound familiar?

It's certainly happened to me, and many others I've seen. If it hasn't happened to you yet, consider yourself lucky, or maybe you've already found a way to make your selectors resilient 😉 .

This is a problem we've faced too often at FloQast, but we've made some strides to combat it, and I'd like to share an approach we've taken and what I've found.

Levels of Brittle - How selectors are prone to break and examples of what breaks them

CSS selectors will be used in all the examples, except where noted. The same principles/approach can be applied to XPath as well.

First, let's review common selectors and how they can fail us. If you already are familiar or want to skip this part, feel free to scroll to Data Attributes below!

An Example of a Worst Case

div > div > div > div:nth-child(2) > div > ul > li:nth-child(5) > div > div + div > div > button

This one just gives me nightmares 😱 No! Careful! Don't blow on it!

Precarious cards on top of each other like such brittle selectors
No, please no!

Okay, I haven't seen exactly that one, but similar ones! And they all basically made me feel just like that.

Why? It's definitely specific. But it's built with a strict sequence of generic pieces.

It's sort of like saying search a map of a city and find a specific kind of store inside a building inside a shopping block that's the first one that's right after another shopping block on its street which is the 5th street in the second region… well you get the point.

Let's look at some of these and other brittle selector pieces and examples of ways they can break.

Easily breakable

In our analogy, it was specific and may bring you to the desired store, uniquely matched by that combination (or just happens to be the first that matches it). Same with that selector.

Here are some types of brittle locator pieces, starting with the most brittle:

  • Child operators, indexes, and other precise relational pieces - * > * + * *:nth-child(n)
  • Tag names - div ul li div button a
  • Some attributes - [disabled] [checked] [type=text]
  • Text - (using xpath since only way to use text) //*[contains(text(), “June”)]

I have seen all of these break, and not uncommonly. Here are some ways they can break:

  • An element is added or removed, breaking relational pieces (or text!)
  • A tag name is changed - this is easy for a developer! For instance, a td, li, or even a button can be changed to a div and styled to work/look the same.
  • An attribute is modified or no longer exists
  • Text is updated, whitespace causes issues (seen with element additions), or if you internationalize and want to test in any of the languages

While these are very brittle, some less brittle commonly used selectors are still available.

More resilient - classes

Of the less brittle variety of common selectors, classes (.month-picker) are probably the most common. Each one can be on as many elements as needed, be dynamic, and really can handle most cases. Though not as resilient as ids, these are still relatively resilient and are more flexible and easily used. If you don't have more resilient options, classes are generally great to use, and as many as desired can be added without affecting anything if they have distinct names.

There's just one main problem with these: Their main purpose is for styling and app code!

  • If the company switches to something like styled components, the classes can be unreadably morphed, or at least added to (like pre-indexed with names)
  • Developers may add, remove, and rename them anytime based on their needs
  • Developers likely won't know which ones are actually used by testing or other purposes
    • Unless you create and mark ones specifically for testing, which makes these a better option, but still can involve some risk and other issues

Most resilient of commonly available approaches - id

An id (#export-button) is the most resilient option of the common selectors, as well as the simplest. Why? For one main reason - they target a unique element on the page (or at least they should). Thus if you want to target that specific element, you can just use the id, and it won't select anything else. Because they're so targeted and not used as much as classes for styling purposes, they're also less likely to be changed or removed, and are usually clear as to what they are.

There are some cons though:

  • There are usually not that many of them, and thus they don't help with the majority of selectors
  • Not helpful for non-unique elements
  • Not good/meant for storing dynamic values

Is there a way we can have super resilient selectors that are flexible for any potential need as well as being able to add as many as we want?

Data attributes to the rescue!

Data attribute selectors to the rescue!

One of the main approaches we've taken at FloQast to make our selectors more resilient and flexible is to take advantage of data- attributes.

What are data attributes?

Data attributes are any html attribute that starts with data-. They are completely ignored by anything html related, and thus have no risk of affecting the page or functionality from anything else. And you can create as many as you want!

How have we used them?

While we have used them for various purposes, like storing a component name on its outermost element or the role of an element within the component, we also met and agreed on setting aside a QE specific 'namespace'! We have control over and are free to have as many attributes as we want that start with data-qe-.

This allows us all sorts of reliable, yet flexible selectors. Want to mimic a standard css id to select a unique element? data-qe-id="export-button"! Want to select a menu item without having to use text? [data-component-name="month-picker"] [data-qe-option="March"]! How about a specific row of a table? [data-qe-account-row="1000 Cash"]! You can even store and pull data, not just for selecting!

Why are these so helpful?

  • They're a lot less likely to change. Ones dedicated to a specific purpose like data-qe-* are virtually unbreakable because no one should have any reason to touch them for any other purpose!
  • If named well, they can be very clear to what they are, and improve the readability of your code
  • Both name and value can be specified with these!
  • They can be added without any risk to the app or user
  • Related to a point above, but since ids and classes are primarily meant for styling and app code and can be easily changed by developers not realizing specific ones are meant for another purpose, it disentangles the two uses and resolves any related issues
  • They can contain names and/or data that doesn't make sense as ids, classes, etc., and can also remove the need to select by text

So while not as short as id (#export-button) or class (.month-picker), and limited to string values, they're still succinct and can provide even greater clarity and flexibility.

We started using these over a year ago. Since we introduced them, they have proven more and more valuable and needed, such as when classes started disappearing (or rather getting mangled) from using styled components. Having the pattern already in place prepared us for those changes.

Summary

Selectors are one of the most common reasons automated UI tests fail. Seemingly insignificant code changes can wreak havoc on unsuspecting tests whose selectors are too brittle. Avoiding the most brittle ones, such as using relational operators, makes a big difference, but we can also create practically indestructible selectors with the use of data- attributes.

While we're not the only ones to use a similar approach (data-testid is becoming a more popular concept), I've greatly appreciated our data-qe- namespace approach that doesn't limit to ids, giving incredible flexibility and clarity to our attributes and selectors. And I don't have to dread the inevitable doom of brittle selectors as much anymore 😌 .

We've also implemented other strategies to strengthen and make our tests easier to maintain, such as not only Page Objects, but Page Component Objects as well, which includes targeted selectors within specific containing elements. That's a bigger topic overall than the scope of this post however. While we don't have a post for that at time of writing, check out some of our other articles such as a recent one about Assertion Consolidating!

David Rosevear
An advocate for quality with a passion for mentoring and workflow automation, David loves learning, looking to the future, and discovering new ideas and opportunities for improvement. He is an SDET on the QE Core team at FloQast.

Check out research, videos, case studies, and more!

Learn more about working at FloQast!