Selenium is a suite of services and tools you can use to run scripted automation tests in web browsers to verify the functionality of your web applications. Writing Selenium scripts can be challenging, but they get especially complicated if your script needs to find and select page elements that have been altered by backend changes or by dynamic changes via a JavaScript or CSS library.
New Relic Synthetics provides sophisticated scripted browser monitoring built on Selenium, and we rely on that functionality to test our own product. As an engineer on the Synthetics team, I know first hand how important it is for our scripted browsers to be consistent, reliable, and readable—especially if an alert triggers a late-night debugging session.
Fortunately, there are ways to make Selenium scripts more resilient, so you won’t have to worry if future updates to your web app changes the structure of the pages you test.
Two common selectors: XPath or CSS?
Selenium offers several locators, two of which are commonly used: CSS and XPath. Both of these locators point to elements in the Domain Object Model (DOM)—a programming interface that represents a visual model of a page’s structure—but they behave slightly differently. XPath, for example, includes some capabilities that CSS doesn’t, such as being able to traverse from parent to child elements. The XPath locator can also traverse the DOM in older browsers. So, for instance, if you’re running scripts against IE8, you may want to use XPath.
For modern browsers, however, this choice is often a matter of personal preference. You do need to pick a strategy, though, and stay consistent so anyone who might use your scripts can read and maintain them.
This post will focus on CSS selectors; they’re readable, accessible, and prominent—after all, you can find them on every website. Just remember that good CSS on your application’s pages does not necessarily translate to good CSS in your Selenium selectors. For example, web developers typically change a page’s markup and its CSS in tandem. While that won’t necessarily break selectors on the page, it can lead to broken selectors in your Selenium scripts. Similarly, when developers use CSS to style a page, they target all elements that match a selector. But in your Selenium script, you’re looking to isolate a specific element to validate its contents or to select it. These different goals lend themselves to different CSS usage.
Make your life easier with developer tools
Before we dig into examples of using CSS selectors in Selenium scripts, I want to point out a tool in Google Chrome—as well as other browsers that come with developer tools—that can make writing scripts much easier.
In Chrome, you can copy an element's CSS selector to your clipboard and then paste that directly into your script. To do this, open Chrome’s developer tools (View > Developer > Developer Tools), inspect the desired element, and then right-click that element (in the example in the next section, I choose an h1
element), and select Copy > Copy Selector. This convenient Copy Selector shortcut is often the quickest way to include a CSS selector in a script without having to worry about introducing typos or altering the DOM structure of your page. (Note that the exact steps required to perform this task may vary from browser to browser.)
Examining a selector
Let’s take a closer look at a CSS selector.
On the page shown below, I’m going to choose the “Catch problems before your customers do” heading—an h1
element—which results in the following CSS selector:
#main-content > div.p12-text-image-header > section > div > div > div.col-sm-12.col-md-7 > div > div > div > h1
The selector gives you the entire DOM tree path to the selected h1
, but it’s extremely verbose. It’s also pretty generic and provides little useful context for writing or debugging a Selenium script. This type of verbose selector is also fragile, as it adds more elements to the selector. Adding more elements to the page could disrupt the selector and break a script.
Why context matters
Let's say that our marketing team is adamant that this page always retain this heading, as it’s great for conversions. In that case, we’d want our test scripts to verify that the main heading doesn't change and remains an h1
element.
One way to ensure we're targeting this section is to make the selector more specific and less verbose, and keep it closer to the target. For example, let’s look at our original selector:
#main-content > div.p12-text-image-header > section > div > div > div.col-sm-12.col-md-7 > div > div > div > h1
At the end of the selector sits the h1
element we used as our target. It's the child of a div;
and several, mostly nameless, div
tags make up the rest of the selector, which doesn't offer much context. Looking at the DOM tree surrounding our h1
element, we see the following:
If we take a closer look at this example, we see an all too common situation: Nearly all of the elements use classes instead of IDs. That’s why the selector for the h1
element is so verbose.
Here’s one way to work through this situation to create a useful selector:
First, cut any implied elements or elements that don’t provide context on their own. For example, the #main-content
element isn't really necessary because it simply implies that the element is in the main section of the page (and not in, for example, the header or footer), and I always expect the hero section (in which the h1
sits) to be in the main section. To make this element selector more specific, make sure the h1
element is always part of the hero section and can be validated as such. To do this, replace the original selector with something like .hero h1
.
This targets the h1
element that's somewhere inside of an element with the class of hero (which defines the hero section). This should be sufficient given the conventions surrounding how we h1
tags are used in content creation (for example, there shouldn’t be more than one on a page). But because often we don't control the markup for what we're monitoring, you can include a CSS pseudo-class in your selector to help make sure you target the first h1
element in the hero class. This pseudo-class will look like something like .hero h1:first-of-type
.
Making a slight adjustment like this to a selector specifies exactly what you’re looking for, and it also makes your script more resilient if anyone makes changes to your web application.
Four guidelines for simple scripts
Of course, no one can make Selenium scripts 100% impervious to changes in web apps, but following four simple guidelines can help to make your scripts more robust and consistent.
- Use the most specific attribute you can for an element. If an element has an ID, use that ID instead of a class or name, as IDs are unique identifiers for elements on a page.
- When choosing a selector, get as close to the target as possible, and provide plenty of context for what you expect the selector to do (as shown above). Clarity of intention will make your script easier to maintain and debug.
- Keep your selectors as short as possible, and make them specific. Use CSS pseudo-classes, but use them sparingly to avoid obscuring the context of the selector. For example,
div:nth-child(5)
tells us that the 5thdiv
of the parent will be selected, but it doesn't give us much insight into the contents of thediv
. - If there's no way to keep your selectors short, provide as much context as possible with verbosity.
Your web applications are dynamic; they evolve just as your business does. These simple tips should enable you to create more versatile and resilient Selenium scripts.
Try your Selenium scripts with New Relic Synthetics
New Relic Synthetics lets you proactively monitor your web applications with Selenium-powered Google Chrome browsers built using the Selenium WebdriverJS library. This is a flexible way to expand and automate your testing, monitoring, and alerting. You can use New Relic scripted browsers to run your scripts from one (or all!) of Synthetics’ public or private monitoring locations around the globe, and get a complete overview of where your application is functioning as expected and where it might need some attention.
The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic. Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic. Please join us exclusively at the Explorers Hub (discuss.newrelic.com) for questions and support related to this blog post. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.