"Change breaks the brittle." — Jan Houtema
I love this quote, though I'm not quite sure if "Jan Houtema" is a real person. It may be a Paul Graham pseudonym... But in any case, yes, change breaks brittle things and one of the challenges of automated testing is to mitigate that effect as much as possible while you design tests for an ever-changing application. This is especially true with end-to-end testing that's being done in a web browser because you're interacting with your application through its GUI and touching various layers.
I talk more about the "maintenance tax" associated with automated testing in a previous post titled "Where Does End-to-End Testing Fit into a Comprehensive Testing Approach?" In this post I'm going to cover one way that maintenance tax can be reduced — through the use of durable, well-designed CSS selectors to target elements in our web application.
I don't intent to simply explain CSS syntax here. I'm also going to focus on when and where each of these different CSS options make sense within your tests. Mozilla's Developer Network has thorough documentation on CSS selectors that I highly recommend bookmarking. For this post, I'm going to start off with a brief overview of what CSS selectors are in my own words.
There's no magical method for easily pinning down an element in every situation. You're essentially building a technical "description" so that the element, or elements, can be pinpointed. In some cases, the element may have a unique attribute (like an ID) that makes this easy. In other cases, the attributes are sparse or broad and need to be chained together. Occasionally hierarchy needs to be used as well.
Think of it like this... You're looking at a crowd full of 1,000 people. A few of these people are wearing a sticker with an ID number on it. A few others are wearing a name tag. Most don't have either. You're asked to verbally identify some of the people in this crowd. How do you do that? For the folks with an ID or name tag, it's straightforward. But for those without? You need to build a list of attributes, and possibly surroundings, to identify them: "The tall, male in the blue suit next to the woman in the red dress." In this analogy, the crowd full of 1,000 people is the DOM of a web page (full of 1,000 elements) and the verbal description is your CSS selector for targeting them. It's a little bit like this old classic:
CSS provides a syntax for designing these selector strings by specifying attributes and hierarchy of elements so that you can match exactly the element(s) you intend. I will walk through some of the more common options, explain the syntax, and offer thoughts on when they're useful.
The ID attribute is, in most cases, the most ideal way to match an element when it is available. It's defined like this:
<input id="email" value="">
It can be matched using this syntax in your selector:
I'm just putting a
# in front of whatever the
id attribute is set to. Super easy.
In theory, IDs are unique on the page and remain static from page load to page load. This means that there should be only one element on the page with its
id set to
id set to
But, of course, there are exceptions... If you or your engineers are being naughty (tsk tsk), you may end up with more than one element on the page with the same ID. This is generally "wrong", but I've seen it happen. Sometimes multiple responsive views are duplicated in the DOM which are hidden or shown depending on screen size. Other times, I've seen multiple login forms duplicated in the DOM which share the same IDs on their form inputs. Again, these scenarios "shouldn't happen" but they do, so just be aware.
Another situation to be aware of is "dynamic" IDs, meaning IDs that change each time the page is loaded. That makes them useless for targeting since they're different every time. From an engineering perspective, this isn't an ideal use of IDs in my opinion, but I see it quite a bit. Things like this:
<input id="email-28742" value="">
-28742 portion is a pseudo-random number or string that changes each time the page loads... If I specify my selector as
#email-28742 and run a test, the ID may be something like
email-32472 when the page loads so my selector won't find anything. Womp womp.
EmberJS is a notorious culprit here. Their framework seems to leverage dynamic IDs a lot... I'm sure they have a good reason for it... right? right?! Fortunately, you can work around this problem. In the case of our product, Ghost Inspector's recorder has some logic and options built-in to avoid dynamic IDs. You can also use the "partial matching" attribute selectors that I talk about below, or just avoid the ID entirely and locate the element using other methods.
An attribute is essentially any property of an element. Those familiar with CSS tend to think of things like the
name of an input element. For example, consider this element:
<input name="email" value="">
Attributes can be specified in your selector by taking the attribute assignment and dropping it into square brackets, like so:
That selector will look for any element on the page with a
name attribute that's set to
name attributes are often (but not always) unique on the page.
Attribute selectors can be used for far more than names, however. In fact, the ID identifiers that we covered earlier are attributes themselves. The syntax in the section above is essentially shorthand. We could also match an ID using this selector
[id="identifier-here"]. It's the same as
Apart from the
name attribute that I've mentioned, other useful attributes include input
value. For instance, to match a submit button like
<input type="submit" value="Send"> or a checkbox like
<input type="checkbox" value="true">, you could use
[type="checkbox"], respectively (though additional identifiers may also be necessary).
value attribute can also be useful. For instance, assume we have this in the DOM:
<input type="radio" name="point" value="1"> <input type="radio" name="point" value="2"> <input type="radio" name="point" value="3">
Suppose we want to click the radio input the value of 2. This selector might work nicely:
Hey, look! I've got 2 sets of brackets in there! I'm matching by both
value. Yes, you can do that. You can combine as many attributes as you want — and, you don't need to stick to just attribute selectors either. As we explore more options and syntaxes below, you'll see that we can do things like
input[name="something"].some-class.another-class which combines element tag, attributes and classes. For now though, let's continue talking about attributes.
In some cases, I've also found it useful to target elements using their
placeholder attribute, which specifies the label that appears inside of an input element when it's empty. For an element like
<input name="email" placeholder="Email Address">, you might consider using the selector
[placeholder="Email Address"], which is nice and semantic. I've seen situations where using
data attributes, as in
[data-something="xyz"], can be useful as well. You can get a little creative here!
So, attribute selectors are very useful when working with form elements — but they don't stop there. They can also be used for other things, like matching the
href property of anchor tags (links) on the page. A link like
<a href="/docs/">Docs</a> could be matched with
[href="/docs/"]. You can use attribute selectors to match any property on any type of element.
Ok, great. On to the next thing? Oooh no.
One of coolest features that attribute selectors provide (and the most underutilized, in my opinion) is the different operators that are available. The operator is that
[name="email"] which looks for a
name attribute that is equal to exactly
The 3 that I use the most are
^= (match at the beginning of the attribute),
$= (match at the end of the attribute), and
*= (match anywhere in the attribute). Let's look at how this works:
<a href="/user/12345/edit/">Edit User</a>
I want to target that link with a selector. The problem is, the
12345 portion is different every time because it's a new user ID that I'm creating during the automated test itself. This means using
a[href="/user/12345/edit/"] won't work. How do I get around it? I match the beginning and end of the link URL, like this:
See what I did there? We've "weeded out" the dynamic part. Now I'm just looking for
/user/ at the front of the URL using
/edit/ at the end using
$=. This is very, very powerful especially in automated testing! Let's look at another example:
Now we've got a URL with a domain in front and a dynamic session ID at the end. Obviously I can't have that session ID in my selector, since it changes. But, to add another twist, suppose I'm building an automated test that I want to run on multiple environments — not just
staging.company.com but also
www.company.com and others. I want to take that domain out of the selector as well. I can do this:
a[href*="/dashboard/"]This time I'm using
*=to just look for
/dashboard/anywhere in the URL.
Attribute selectors can also be used deal with the dynamic ID problem that I mentioned above. The example element I used
<input id="email-28742" value=""> could be matched with
[id^="email"] to ignore the dynamic portion.
This is about as close as CSS gets to "regex" type matching so make a note of this functionality and use it wisely. It's very useful for making your anchor selectors more durable, among other things.
Elements can have a
class attribute that contains a space-separated list of defined "classifications" that apply to the element. This is most often used for styling elements in the stylesheet — but in some cases, classes can be useful for targeting elements in our browser tests. The
class attribute is used like this:
<div class="some-class another-class">...</div>
To match that element using a class name, we use this syntax in our selector:
.some-class. We could also require both classes for the match, combining them like this:
.some-class.another-class. As with IDs, classes are just another type of attribute, so the
.class-name syntax is essentially shorthand for this:
We need to be picky about using classes in selectors because, in most cases, they're designed to apply to groups of elements — not just one. Since we're generally interested in matching a specific element with our selector, adding broad class names which apply to lots of elements (for example, Bootstrap's
.form-control class which gets applied to all form inputs) isn't very helpful. It just clutters up the selector without making it any more specific.
You'll also want to avoid classes that look temporary or are very style-oriented. For instance, a "grid" class like
.col-md-1 could easily change to
.ng-whatever type class names. I'd recommend avoiding those in your selectors as well.
With that said, some classes can be useful especially when they provide a good amount of specificity or semantic value. If you're interacting with a form on a page that has a number of buttons, but just one with a
.btn-submit class, that might make a good selector. If you're interacting with a large form that has multiple sections with different class names, using something like
.intro-section within your selector might help with specificity (see the hierarchy section below).
By element tag, I mean
input for an input field,
a for an anchor,
h1 for a header and so on. These can be placed at the front of your selector, like so
a[href="/docs/"]. It's rare to use just the element tag as the selector, but it could make sense in some situations. For instance, suppose your page has a single
<h1>...</h1> element and you want to assert the contents. If you don't expect any more
<h1> elements to be added, then sure, use
h1 as the selector.
You may also have some kind of scenario like in this (contrived) example:
<div class="username">Justin</div> <span class="username">Justin</span>
In this case, using
.username will match both elements, since both have that class. You could use
span.username to pin it down to the one you want to target.
Ok, so beyond those simple scenarios, when is this useful?
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler
In most cases, the usefulness of adding this has less to do with the targeting process and more to do with conveying clarity to anyone looking at your selectors. For example, consider this selector:
[name="level"]. Now, if there's only one element with the name
level on the page then this selector is perfectly adequate for targeting purposes. But, if I'm a new engineer looking at that selector, I don't know whether it's targeting an
<input> field, a
<select> box, a
<textarea>, or something else entirely... Personally, I like the idea of seeing
select[name="level"], not because it necessarily helps with targeting, but because it tells me something useful about the element (and probably the action being performed).
You could make a case that this makes the selector slightly more fragile... Perhaps an
<input> field that you're targeting gets changed to a
<textarea> in an application update, so the selector breaks because you've used
input[name="address"] and now it needs to be updated to
textarea[name="address"]. That's a fair argument... so I'd say to use your own judgment and preferences here. I like to include the tag when I feel like it conveys something useful to people working with the test. In my case, that's typically
<a> and occasionally a few others depending on the situation, like
Hierarchy in your CSS selectors should be used with caution because it immediately increases the complexity. With that said, using hierarchy is necessary in many situations — and can actually improve clarity in a few.
Here's a simple DOM illustrating hierarchy where we've got
<label> elements that are nested inside of (children of)
<div class="name"> <label>Justin</label> </div> <div class="email"> <label>firstname.lastname@example.org</label> </div>
Let's assume we want to target the
<label> that contains "Justin". How do we do that? We can't use just
label as our selector because that will match both
<label> elements. We need to narrow down the parent element first. That selector would look like this:
I match the parent element using its
.name class. I add a space afterwards. This indicates that I'm going to target an element contained within the element matched by the first string. After the space, I match the inner element. In this case, I'm just using the
label tag name. This selector essentially says, "Find the
<label> element somewhere inside of an element with a
I can define as many levels of hierarchy as I'd like in a CSS selector using multiple spaces. In a large DOM, I may end up with something like this:
#supportForm .comments textarea
In that selector I'm looking for a
<textarea> element, which is inside of an element with a
.comments class, which is then inside of an element with an ID set to
#supportForm. Of course, if the
<textarea> element had a unique
name attribute, I could make this much simpler... but that's not always going to be the case.
In addition to using a space between child elements, I can also use an angle bracket like this:
.comments > textarea. When I use the angle bracket, it means that the element is a "direct child" of the parent element. In other words, it's in precisely the next "level" of nested elements — not just anywhere inside. Let's illustrate this with another (contrived) DOM:
<div class="comments"> <textarea></textarea> </div> <div class="comments"> <div class="something"> <textarea></textarea> </div> </div>
If I use the selector
.comments textarea, it will match both
<textarea> elements. However, if I use
.comments > textarea, it will only match only the first, since the second block has a
<div> nested in between the two which breaks the "direct child" relationship.
The angle bracket can be useful for precision, but can also make selectors more and more brittle because you're effectively taking away the flexibility of the selector. Adding an extra
<div> layer (as in the example above) is a common occurrence, so you're more prone to breakage when application updates occur. When hierarchy is a must, I'd avoid using the angle bracket unless you really need to (which shouldn't be often). Sticking to spacing and broader relationships will help to keep your selectors more durable as your application's DOM changes.
In some case (most often in lists) it can be useful to target elements based on their position in a sequence. Here's a simple DOM illustration:
<ul class="fruits"> <li>Apple</li> <li>Orange</li> <li>Pear</li> </ul>
I want to target the "Orange" entry. How do I do that? The
<li> elments don't have any distinguishing attributes... Fortunately, CSS provides an
:nth-of-type() feature to target elements by their position in a list. For this example, my selector would look like this:
I simply plug
2 into the
:nth-of-type() function since I'm looking for the second
<li> element in the list. Simple, right? CSS also provides a couple of other similar features that can be used. They're called pseudo-classes. These are handy for their respective use cases. Just be careful about using these features in situations that they're not meant for, or where the ordering of your elements may be changing frequently. Note that if the order of the items above changes, then my selector may no longer be targeting the "Orange" entry.
If you look at the DOM example in the section above, wouldn't it be nice and semantic if we could target that
<li> element using its "Orange" text? That would be nice, wouldn't it?... I wish I could tell you that CSS has a tool for that, but unfortunately, standard CSS does not. You may have seen something like this before:
In principle, I love that selector. It's clear to humans and it won't break if the order of the list elements changes — semantic and durable. Unfortunately, the
:contains() pseudo-class is something that's only available in the Sizzle selector engine (popularized by jQuery). It's not available in your browser by default right now. Very sad, I know... Early versions of Selenium injected the Sizzle engine for you, so you could use this feature in your tests. However, with Selenium v2 and up that is no longer done. At Ghost Inspector, we've built this into our system — but it's only available in some of our browser options, not all.
The best solution I can offer right now is to consider mixing in XPath selectors which support matching by text contents. XPath is a method for element selection that has a different syntax than CSS, but is used in the same way. It deserves its own blog post in the future. In the meantime, the XPath selector for the example above would look like this:
XPath is supported by both Selenium and by Ghost Inspector, so this is a good option when matching an element by its text contents makes sense — which is fairly often. For more details on using XPath, check out our CSS to XPath Conversion Guide.
Welp, that's a tough one. The most obvious answer is "experience". As you work with HTML, CSS and the DOM more often (and in different situations), you'll start to get a handle on what might work for different scenarios. But experience takes time, so let me offer some other advice.
I've designed this post to (sort of) walk through selector options from most ideal (simple and durable) to least. There are exceptions to this logic, but I usually take this approach when building a selector:
idattribute? Use that.
nameattribute? Use that.
placeholder? Use that.
href? Use that.
idattribute or makes more sense semantically.
Another hugely helpful asset in deciding how to build these CSS selectors for your tests is your own knowledge of the application that you're testing. What do you know about the way the DOM is designed AND what do you know about the way it may change in the future? Are portions of the UI more active than others? What's likely to change? Are class names changed frequently? How does the engineering team think about the DOM? Ask them! Try to get a handle on how they go about changing the DOM and factor that into your own considerations.
This is a tricky question that I get asked quite a bit. Believe it or not, you can always match an element on the page uniquely with a CSS selector. However, that selector may end up being super ugly and brittle with no better alternatives available.
If you find yourself in a situation where there's no "good" selector available to locate an element, is it acceptable to have the engineering team add an
id or some other attribute that you can use? This can be really handy. Obviously a unique
id makes for an easy selector. I've also heard folks suggest the use of
[qa="id-here"] type attributes specifically for targeting during testing.
I have mixed feelings about this approach. It can make your life much easier, but also comes with trade-offs... If you decide to go this route, make sure everyone understands those tradeoffs. Here are the two downsides that I see:
If you're a Ghost Inspector user, we've got a test recorder for your browser that will generate CSS selectors for you as you record a test. However, those selectors are generated algorithmically and are meant to be reviewed afterwards. The recorder doesn't know anything about your application or what's dynamic, so they're often going to be a bit more rigid than necessary. In some dynamic cases, they simply won't work without modification. Chrome too has a built-in option for generating a CSS selector by clicking on the element in using its developer tools:
Get familiar with that line of code! It's incredibly useful for figuring out exactly what your CSS selector will match on the page, both when building selectors and when troubleshooting them. You can also use
document.querySelector() to match a single element, but I recommend using
document.querySelectorAll() during your development process to help ensure that you're only matching the element you expect, and not others (which will happen often). Sławomir Radzymiński has a blog post on finding and testing CSS selectors that outlines this process in a little more detail.
A couple of quick tips if you're running into problems with a CSS selector:
document.querySelectorAll()to ensure that the selector is matching exactly (and only) what you intend on the page?
iframe? Each frame has a completely separate DOM, so that complicates things. Unfortunately, that's outside of the scope of this post, but if you're using Ghost Inspector, see our documentation.