Hi! I’m Takuya Suemura, Quality Evangelist at Autify.
5 years ago, I wrote an article titled Why you shouldn’t use ids in E2E testing. This article has been viewed for a long time and has become one of the most popular articles on our blog.
On the other hand, five years is more than enough time to witness change in software development trends. In terms of testing tools, Playwright is a new option, and element search is emerging as a new best practice
This article will review the evolution of the concept of element traversal in E2E testing and describe best practices.
First, let's review the concept of element locating. Element locating refers to the process of locating specific elements within a web page, such as CSS selectors, XPath, and so on. The string used to select these elements is collectively called a locator.
Here’s an example of a CSS selector using the btn-primary
class in the target element.
<button class="btn-primary">Submit</button>
// Specify the element that has btn-primary class
driver.getElementsByclassName("btn-primary")
This is the most primitive element locating strategy. It has some issues on element uniqueness and referencing internal structure.
Uniqueness is essential for good locators. It means that one locator always matches one element.
The class
attribute shown is not unique as it is tightly related to page styling, could refer to multiple elements, and frontend development could cause the need for it to be changed.
Hence, we need to use a unique attribute. id
was the best practice for a long time - because id
MUST be unique in a page.
(ref. MDN https://developer.mozilla.org/en/docs/Web/HTML/Global_attributes/id)
<!-- add "submit" as the id -->
<button id="submit" class="btn-primary">Submit</button>
// Locate the target element by id, not class
driver.getElementByid("submit")
Using id
’s over class
’s looks great at first glance but both attributes are internal structures of HTML. Internal structures create other problems with element locating.
E2E test validates external behavior of a system. Thus, all locators MUST refer to an external behavior. If we use internal structures, we will be forced to fix test code every time we change internal code. By using internal HTML structures like id’s or class’s, it either inhibits code refactoring or we have to go in and fix our test code every time internal codes attributes change, costing us an immense amount of maintenance time and energy.
For example, here’s a test code that references id
.
<label for="email">Email</label>
<input id="email" />
// Locate by ID
driver.getElementByid("submit")
But if you want to change the id for some reason, you have to change the test code as well, even though you keep the external behavior, although your change does not affect to customers.
<label for="form-123">Email</label>
<input id="form-123" />
// Locate by ID
driver.getElementByid("form-123")
Ideally, E2E test code must follows the real user journey. But in this case, the E2E test code follows your frontend code.
So what should we use instead of id
and class
?
E2E tests check the external behavior of the target system. In other words, writing E2E tests means writing how a user would use the system in working code, with browser automation technology. Ideally, locators in E2E test must be the same as how users would find elements.
How about Element text ? Most users will find an element by it’s text. Let’s say there are two buttons that don’t an have id
or class
, but they do have button labels. Users can distinguish them by these labels text. In the example below, we can use the text “Next” and “Back”.
<button>Next</button>
<button>Back</button>
How about using an element’s Role? Most HTML elements have an implicit role that we can use as a locator.
For example, <input type="button"/>
and <button/>
are different in terms of tag name, but both have a button
role. From the POV of users, it does not matter whether the button is implemented with an input
or button
tag.
// both has "button" role
<input type="button" value="Next"/>
<button>Next</button>
For elements that don’t have implicit roles like <div>
, we can specify it with the Ariarole
s (role
attribute).
Note that Aria roles are completely optional. In other words, abusing role
and any other ARIA features may break accessibility. When you use ARIA, you should always need to consider the accessibility, not testability.
Ref: Read Me First - No ARIA is better than Bad ARIA
How about using element Structure? I mean, we can distinguish elements with the parent element. For example, even though there are two elements for “email”, we can distinguish them by what the parent element is - <nav>
and <main>
.
<nav>
<form>
<label for="email1"> email </label>
<input id="email1" type="text" name="email"/>
<label for="password"> password </label>
<input id="password" type="password" name="password"/>
<input type="submit" value="Submit" />
</form>
</nav>
<main>
<form>
<label for="email2"> email </label>
<input id="email2" type="text" name="email"/>
<label for="password2"> password </label>
<input id="password2" type="password" name="password"/>
<input type="submit" value="Submit" />
</form>
</main>
The quickest way to implement role-and-semantic-based element locating strategy is to use Testing Library. It is a toolkit to implement best practices of automated testing, and it provides some useful APIs to realize the strategy.
Here’s some of the API locators that Testing Library provides.
It also provides the within
API locator to find an element inside a specific element. With it, you can do structure-based element locating.
Testing Library also provides a guideline on how to use the APIs. It’s fantastic source of information for any software tester.
https://testing-library.com/docs/queries/about
In addition to Testing Library, some E2E test frameworks like Playwright also supports equivalent APIs.
https://playwright.dev/docs/locators
Playwright’s locators are kind of a subset of a Testing Library, but it’s simplified. You can find it in the https://playwright.dev/docs/testing-library#replacing-within.
By the way, how would you find roles and accessibility attributes to use getByRole
, getByLabelText
and so on? They are not in the DOM tree. You need to see the Accessibility Tree.
As an example, the following web site has the navigation bar that includes a form. The form also has some text areas and buttons. You can see structures and attributes of the accessibility tree from Chrome DevTools.
In this article, I discussed the modern best practices of element locating for UI/E2E web application test and how it has changed. By using attributes and semantics that are accessible for users, we can write more robust test code that is safe from the internal changes and makes maintenance easier.
Quoting a post by Kent C. Dodds, creator of Testing Library, and one of the thought leaders of web frontend tests.
The more your test resemble the way your software is used, the more confidence they can give you.
I hope this article helps you make your tests “resemble the way your software is used”!