{"id":364218,"date":"2022-04-22T11:45:38","date_gmt":"2022-04-22T18:45:38","guid":{"rendered":"https:\/\/css-tricks.com\/?p=364218"},"modified":"2022-04-22T11:45:40","modified_gmt":"2022-04-22T18:45:40","slug":"front-end-test-element-locators","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/front-end-test-element-locators\/","title":{"rendered":"Writing Strong Front-end Test Element Locators"},"content":{"rendered":"\n
Automated front-end tests are awesome. We can write a test with code to visit a page \u2014 or load up just a single component<\/a> \u2014 and have that test code click on things or type text like a user would, then make assertions about the state of the application after the interactions. This lets us confirm that everything described in the tests work as expected in the application.<\/p>\n\n\n\n Since this post is about one of the building blocks of any automated UI tests, I don\u2019t assume too much prior knowledge. Feel free to skip the first couple of sections if you’re already familiar with the basics.<\/p>\n\n\n\n\n\n\n There\u2019s a classic pattern that\u2019s useful to know when writing tests: Arrange<\/strong>, Act<\/strong>, Assert<\/strong>. In front-end tests, this translates to a test file that does the following:<\/p>\n\n\n\n In specifying what to interact with<\/em> and then later what to check<\/em> on<\/em> the page<\/em>, we can use various element locators<\/em> to target the parts of the DOM we need to use.<\/p>\n\n\n\n A locator<\/dfn> can be something like an element\u2019s ID, the text content of an element, or a CSS selector, like We often evaluate locators in terms of being brittle<\/em> or stable.<\/em> In general, we want the most stable element locators possible so that our test can always find the element it needs, even if the code around the element is changing over time. That said, maximizing stability at all costs can lead to defensive test-writing that actually weakens the tests. We get the most value by having a combination of brittleness and stability that aligns with what we want our tests to care about.<\/p>\n\n\n\n In this way, element locators are like duct tape. They should be really strong in one direction, and tear easily in the other direction. Our tests should hold together and keep passing when unimportant changes are made to the application, but they should readily fail when important changes happen that contradict what we’ve specified in the test.<\/p>\n\n\n First, let\u2019s pretend we are writing instructions for an actual person to do their job. A new gate inspector has just been hired at Gate Inspectors, Inc. You are their boss, and after everybody\u2019s been introduced you are supposed to give them instructions for inspecting their first gate. If you want them to be successful, you probably would not<\/em> write them a note like this:<\/p>\n\n\n\n Go past the yellow house, keep going \u2018til you hit the field where Mike\u2019s mother\u2019s friend\u2019s goat went missing that time, then turn left and tell me if the gate in front of the house across the street from you opens or not.<\/p><\/blockquote>\n\n\n\n Those directions are kind of like using a long CSS selector or XPath as a locator. It\u2019s brittle \u2014 and it\u2019s the “bad kind of brittle”. If the yellow house gets repainted and you repeat the steps, you can\u2019t find the gate anymore, and might decide to give up (or in this case, the test fails).<\/p>\n\n\n\n Likewise, if you don\u2019t know about Mike\u2019s mother\u2019s friend\u2019s goat situation, you can\u2019t stop at the right reference point to know what gate to check. This is exactly what makes the “bad kind of brittle” bad \u2014 the test can break for all kinds of reasons, and none of those reasons have anything to do with the usability of the gate.<\/p>\n\n\n\n So let\u2019s make a different front-end test, one that\u2019s much more stable. After all, legally in this area, all gates on a given road are supposed to have unique serial numbers from the maker:<\/p>\n\n\n\n Go to the gate with serial number 1234 and check if it opens.<\/p><\/blockquote>\n\n\n\n This is more like locating an element by its ID. It\u2019s more stable and it\u2019s only one step. All the points of failure from the last test have been removed. This test will only fail if the gate with that ID doesn\u2019t open as expected.<\/p>\n\n\n\n Now, as it turns out, though no two gates should<\/em> have the same ID on the same road, that\u2019s not actually enforced anywhere And one day, another gate on the road ends up with the same ID.<\/p>\n\n\n\n So the next time the newly hired gate inspector goes to test \u201cGate 1234,\u201d they find that other one first, and are now visiting the wrong house and checking the wrong thing. The test might fail, or worse: if that gate works as expected, the test still passes but it\u2019s not testing the intended subject. It provides false confidence. It would keep passing even if our original target gate was stolen in the middle of the night, by gate thieves.<\/p>\n\n\n\n After an incident like this, it\u2019s clear that IDs are not as stable as we thought. So, we do some next-level thinking and decide that, on the inside of the gate, we\u2019d like a special ID just for testing. We\u2019ll send out a tech to put the special ID on just this one gate. The new test instructions look like this:<\/p>\n\n\n\n Go to the gate with Test ID \u201cmy-favorite-gate\u201d and check if it opens.<\/p><\/blockquote>\n\n\n\n This one is like using the popular This is about as far away from brittle as you can get, and it confirms the functionality of the gate. We don\u2019t depend on anything except the attribute we deliberately added for testing. But there\u2019s a bit of problem hiding here\u2026<\/p>\n\n\n\n This is a user interface test for the gate, but the locator is something that no user would ever use to find the gate.<\/strong><\/p>\n\n\n\n It\u2019s a missed opportunity because, in this imaginary county, it turns out gates are required to have the house number printed on them so that people can see the address. So, all gates should have an unique human-facing label, and if they don\u2019t, it\u2019s a problem in and of itself. <\/p>\n\n\n\n When locating the gate with the test ID, if it turns out that the gate has been repainted and the house number covered up, our test would still pass. But the whole point of the gate is for people to access the house. In other words, a working gate that a user can\u2019t find<\/em> should still be a test failure, and we want a locator that is capable of revealing this problem.<\/p>\n\n\n\n Here\u2019s another pass at this test instruction for the gate inspector on their first day:<\/p>\n\n\n\n Go to the gate for house number 40 and check if it opens.<\/p><\/blockquote>\n\n\n\n This one uses a locator that adds value<\/em> to the test: it depends on something users also depend on, which is the label for the gate. It adds back a potential reason for the test to fail before it reaches the interaction we want to actually test, which might seem bad at first glance. But in this case, because the locator is meaningful from a user\u2019s perspective, we shouldn\u2019t shrug this off as \u201cbrittle.\u201d If the gate can\u2019t be found by its label, it doesn\u2019t matter if it opens or not \u2014 this is is the “good kind of brittle”.<\/p>\n\n\n A lot of front-end testing advice tells us to avoid writing locators that depend on DOM structure. This means that developers can refactor components and pages over time and let the tests confirm that user-facing workflows haven\u2019t broken, without having to update tests to catch up to the new structure. This principle is useful, but I would tweak it a bit to say we ought to avoid writing locators that depend on irrelevant<\/em> DOM structure in our front-end testing.<\/p>\n\n\n\n For an application to function correctly, the DOM should reflect the nature and structure of the content that’s on the screen at any given time. One reason for this is accessibility. A DOM that\u2019s correct in this sense is much easier for assistive technology to parse properly and describe to users who aren\u2019t seeing the contents rendered by the browser. DOM structure and plain old HTML make a huge difference to the independence of users who rely on assistive technology.<\/p>\n\n\n\n Let\u2019s spin up a front-end test to submit something to the contact form of our app. We\u2019ll use Cypress<\/a> for this, but the principles of choosing locators strategically apply to all front-end testing frameworks that use the DOM for locating elements. Here we find elements, enter some test, submit the form, and verify the \u201cthank you\u201d state is reached:<\/p>\n\n\n\n There are all kinds of implicit assertions happening in these four lines. So, we get a lot \u201cfor free\u201d even in a simple test like this, but we\u2019ve also introduced some dependencies upon things we (and our users) don\u2019t really care about. The specific ID and classes that we are checking seem stable enough, especially compared to selectors like But even our short selectors, like For problems one and two, the recommended solution is often to use dedicated data attributes in our HTML that are added exclusively for testing. This is better because our tests no longer depend on the DOM structure, and as a developer modifies the code around a component, the tests will continue to pass without needing an update, as long as they keep the This doesn\u2019t address problem three though \u2014 we still have a front-end interaction test that depends on something that is meaningless to the user.<\/p>\n\n\n Element locators are meaningful when they depend on something we actually want<\/em> to depend on because something about the locator is important to the user experience. In the case of interactive elements, I would argue that the best selector to use is the element\u2019s accessible name<\/a>. Something like this is ideal:<\/p>\n\n\n\n This example uses the byLabelText helper<\/a> from Cypress Testing Library<\/a>. (In fact, if you are using Testing Library in any form, it is probably already helping you write accessible locators like this.) The way to provide this accessible name for a form field is usually through a Because this is invalid HTML, screenreader software could never associate this label with this field correctly. To fix this, we would update the markup to use a real This is awesome. Now if the test fails at this point after edits made to the DOM, it\u2019s not because of an irrelevant structure changes to how elements are arranged, but because our edits did something to break<\/em> a part of DOM that our front-end tests explicitly care about, that would actually matter to users.<\/p>\n\n\n For non-interactive elements, we should put on our thinking caps. Let\u2019s use a little bit of judgement before falling back on the Before we dip into the generic locators, let’s remember: the state of the DOM is our Whole Thing™ as web developers (at least, I think it is). And the DOM drives the UX for everybody who is not experiencing the page visually. So a lot of the time, there might be some meaningful element that we could or should be using in the code that we can include in a test locator.<\/p>\n\n\n\n And if there’s not, because. say, the application code is all generic containers like This topic opens up a can of worms about how accessibility works in an organization. Often, if nobody is talking about it and it’s not a part of the practice at our companies, we don’t take accessibility seriously<\/a> as front-end developers. But at the end of the day, we are supposed to be the experts in what is the “right markup” for design, and what to consider in deciding that. I discuss this side of things a lot more in my talk from Connect.Tech 2021, called “Researching and Writing Accessible Vue… Thingies”<\/a>.<\/p>\n<\/div><\/div>\n\n\n\n As we saw above, with the elements we traditionally think of as interactive,<\/em> there is a pretty good rule of thumb that\u2019s easy to build into our front-end tests: interactive elements should have perceivable labels correctly associated to the element.<\/strong> So anything interactive, when we test it, should be selected from the DOM using that required label.<\/p>\n\n\n\n For elements that we don\u2019t think of as interactive \u2014 like most content-rendering elements that display pieces of text of whatever, aside from some basic landmarks like The HTML we render is where we communicate important contextual information to anybody who is not perceiving the content visually. The HTML is used to build the DOM, the DOM is used to create the browser\u2019s accessibility tree<\/a>, and the accessibility tree is the API that assistive technologies of all kinds can use to express the content and the actions that can be taken to a disabled person using our software. A screenreader is often the first example we think of, but the accessibility tree can also be used by other technology, like displays that turn webpages into Braille, among others.<\/p>\n\n\n\n Automated accessibility checks won\u2019t tell us if we\u2019ve really created the right HTML for the content. The “rightness” of the HTML a judgement call we are making developers about what information we think needs to be communicated in the accessibility tree.<\/p>\n\n\n\n Once we\u2019ve made that call, we can decide how much of that is suitable to bake into the automated front-end testing.<\/p>\n\n\n\n Let\u2019s say that we have decided that a container with the If we add this element and want to write a UI test for it, we might write an assertion like this after the test fills out the form and submits it:<\/p>\n\n\n\n Or even a test that uses a non-brittle but still meaningless selector like this:<\/p>\n\n\n\n Both could be rewritten using This would confirm that the expected text appeared and was inside the right kind of container. Compared to the previous test, this has much more value in terms of verifying actual functionality. If any part of this test fails, we\u2019d want to know, because both the message and the element selector are important to us and shouldn\u2019t be changed trivially.<\/p>\n\n\n\n We have definitely gained some coverage here without a lot of extra code, but we\u2019ve also introduced a different kind of brittleness. We have plain English strings in our tests, and that means if the \u201cthank you\u201d message changes to something like \u201cThank you for reaching out!\u201d this test suddenly fails. Same with all the other tests. A small change to how a label is written would require updating any test that targets elements using that label.<\/p>\n\n\n\n We can improve this by using the same source of truth for these strings in front-end tests as we do in our code. And if we currently have human-readable sentences embedded right there in the HTML of our components\u2026 well now we have a reason to pull that stuff out of there.<\/p>\n\n\nStructure of a front-end test<\/h3>\n\n\n
.blog-post<\/code> or even
article > div.container > div > div > p:nth-child(12)<\/code>. Anything about an element that can identify that element to your test runner can be a locator. As you can probably already tell from that last CSS selector, locators come in many varieties.<\/p>\n\n\n\n
Beginner\u2019s guide to element locators in front-end testing<\/h3>\n\n\n
data-testid<\/code> attribute. Attributes like this are great because it is obvious in the code that they are intended for use by automated tests and shouldn\u2019t be changed or removed. As long as the gate has that attribute, you will always find the gate. Just like IDs, uniqueness is still not enforced, but it\u2019s a bit more likely.<\/p>\n\n\n\n
The DOM matters<\/h3>\n\n\n
\/\/ 👎 Not recommended\ncy.get('#name').type('Mark')\ncy.get('#comment').type('test comment')\ncy.get('.submit-btn').click()\ncy.get('.thank-you').should('be.visible')<\/code><\/pre>\n\n\n\n
cy.get()<\/code> is checking that the element exists in the DOM. The test will fail if the element doesn\u2019t exist after a certain time, while actions like
type<\/code> and
click<\/code> verify that the elements are visible, enabled, and unobstructed by something else before taking an action.<\/p>\n\n\n\n
div.main > p:nth-child(3) > span.is-a-button<\/code> or whatever. Those long selectors are so specific that a minor change to the DOM could cause a test to fail because it can\u2019t find the element<\/em>, not because the functionality is broken.<\/p>\n\n\n\n
#name<\/code>, come with three problems:<\/p>\n\n\n\n
data-test=\"name-field\"<\/code> attached to the right
input<\/code> element.<\/p>\n\n\n\n
Meaningful locators for interactive elements<\/h3>\n\n\n
\/\/ 👍 Recommended\ncy.getByLabelText('Name').type('Mark')<\/code><\/pre>\n\n\n\n
This is useful because now the built-in checks (that we get for free through the cy.type()<\/code> command) include the accessibility of the form field. All interactive elements should have an accessible name that is exposed to assistive technology. This is how, for example, a screenreader user would know what the form field they are typing into is called in order to enter the needed information.<\/p>\n\n\n\n
label<\/code> element associated with the field by an ID. The
getByLabelText<\/code> command from Cypress Testing Library verifies that the field is labeled appropriately, but also that the field itself is an element that\u2019s allowed to have a label. So, for example, the following HTML would correctly fail before the
type()<\/code> command is attempted, because even though a
label<\/code> is present, labeling a
div<\/code> is invalid HTML:<\/p>\n\n\n\n
<!-- 👎 Not recommended -->\n<label for=\"my-custom-input\">Editable DIV element:<\/label>\n<div id=\"my-custom-input\" contenteditable=\"true\" \/><\/code><\/pre>\n\n\n\n
input<\/code> element:<\/p>\n\n\n\n
<!-- 👍 Recommended -->\n<label for=\"my-real-input\">Real input:<\/label>\n<input id=\"my-real-input\" type=\"text\" \/><\/code><\/pre>\n\n\n\n
Meaningful locators for non-interactive elements<\/h3>\n\n\n
data-cy<\/code> or
data-test<\/code> attributes that will always be there for us if the DOM doesn\u2019t matter at all.<\/p>\n\n\n\n
div<\/code> and
span<\/code>, we should consider fixing up the application code first, while adding the test. Otherwise there is a risk of having our tests actually specify<\/em> that the generic containers are expected and desired, making it a little harder for somebody to modify that component to be more accessible.<\/p>\n\n\n\n
main<\/code> \u2014 we wouldn\u2019t trigger any Lighthouse audit failures if we put the bulk of our non-interactive content into generic
div<\/code> or
span<\/code> containers. But the markup won\u2019t be very informative or helpful to assistive technology because it\u2019s not describing the nature<\/em> and structure<\/em> of the content to somebody who can\u2019t see it. (To see this taken to an extreme, check out Manuel Matuzovic’s excellent blog post, “Building the most inaccessible site possible with a perfect Lighthouse score.”<\/a>)<\/p>\n\n\n\n
status<\/code> ARIA
role<\/code> will hold the \u201cthank you\u201d and error messaging for a contact form. This might be nice so that the feedback for the form\u2019s success or failure can be announced by a screenreader. CSS classes of
.thank-you<\/code> and
.error<\/code> could be applied to control the visual state.<\/p>\n\n\n\n
\/\/ 👎 Not recommended\ncy.get('.thank-you').should('be.visible')<\/code><\/pre>\n\n\n\n
\/\/ 👎 Not recommended\ncy.get('[data-testid=\"thank-you-message\"]').should('be.visible')<\/code><\/pre>\n\n\n\n
cy.contains()<\/code>:<\/p>\n\n\n\n
\/\/ 👍 Recommended\ncy.contains('[role=\"status\"]', 'Thank you, we have received your message')\n .should('be.visible')<\/code><\/pre>\n\n\n\n
Human-readable strings might be the magic numbers of UI code<\/h3>\n\n\n