Test IDs. Always.

Why automation attributes are a non-negotiable part of web development

End to end UI tests are the bane of testers and developers alike. They are slow, brittle, and need constant updates. They are also indispensable. A good automation strategy will speed up the feedback loop between a defect being introduced and that defect being detected. All of these benefits, however, need to outweigh the cost of implementing and managing end to end tests.

One step towards creating a good automation strategy is to focus on the weakness of not being able to consistently locate UI elements. Test ids - a fixed UI identifier - reduces issues around UI changes unintentionally breaking tests without a defect being introduced, leading to a better feedback loop in identifying a defect during the CI/CD process and giving more confidence in the testing process.

What is an automation attribute

An autmation attribute or a test id is a specific attribute that sits on a component that both identifies that component on the page and assists the automation framework to interact with that component. you might see it as data-test-id or data-testid, the convention doesnt matter as long as you keep it consistant across your project.

Using a specific attribute for automation allows separation between the implementation of the component and the testing on the component. Class and id are styling attributes, many components use and manipulate these attributes automatically (stylised components as an example), because of this they are likely to change through the process of development.

Using an automation attribute means that the attribute should ideally only change when the component changes in such a way that a new test is required. Styling and refactoring should not affect end-to-end testing unless it has an external and functional impact.

How to implement the test id

Test ids work best when they are unique, readable, and concise. It should be unique so the framework doesn't need logic to figure out where to look, it should be readable because the code needs to be understandable by an engineer on sight, and should to be concise for speed of recognition.

Say your API returns the following invoice json:

json
{
"invoices": [
{
"uuid": "122-23212-3123",
"title": "Shopping",
"description": "Going grocery shopping and",
"cost": 55.54,
"dueDate": 1585872000
}
]
}
tsx
<div data-testid={invoice[index].uuid}>

This is a unique but not readable id. Even someone familiar with the repo would have to look up what this identifier was to understand what their test id is representing.

tsx
<div data-testid={invoice[index].description}>

This one has the potential to not be concise. A description could be very long, and when automated, this would have the potential to break the tests and potentially not show up at all.

tsx
<div data-testid={`invoice-${invoice[index].uuid}`}>

This is most likely the best selector. The invoice part allows the engineer to know it's an invoice and the uuid clearly shows what this is representing.

tsx
<div data-testid={`invoice-${invoice[index].uuid}-${invoice[index].title}`}>

Now for extra points, you might also include the title. This will make it unique but also make it more readable at a glance to the engineer reviewing the test.

Multiple Components on a page

If we were trying to automate an invoice code and the API is sending up the following json:

json
{
"invoices": [
{
"uuid": "122-23212-3123",
"title": "Shopping",
"description": "Going grocery shopping and",
"cost": 55.54,
"dueDate": 1585872000
},
{
"uuid": "122-23212-2324",
"title": "Dinner",
"description": "Going out for takeaway",
"cost": 30.66,
"dueDate": 1586542000
}
]
}

We would want to mock up the code in such a way that the code is interactive.

tsx
<div data-testid={`invoice-${invoice[index].uuid}`}>
<h2 data-testid={`invoice-${invoice[index].uuid}-title`}></h2>
<p data-testid={`invoice-${invoice[index].uuid}-description`}></p>
<button data-testid={`invoice-${invoice[index].uuid}-button-pay`}>
Mark As Paid
</button>
</div>

We could test this in cypress.

typescript
cy.get([data-testid=`invoice-${invoice[index]uuid}`]).should('be.visible');

Changing Components

Sometimes it is important to show when a component's status has changed

tsx
<input type="radio" id="student" name="role" value="student">
<label for="student" data-testid={checked ? 'role-student-checked' : 'role-student'}>Student</label><br>
<input type="radio" id="teacher" name="role" value="teacher">
<label for="teacher" data-testid={checked ? 'role-teacher-checked' : 'role-teacher'}>Female</label><br>

For this example the data-testid is on the label because that is the interactive element. Remember to think in terms of how the user will interact with the page rather than how a computer would. This will help reduce long-term issues around interactivity, and create more accurate outcomes.

Now with the test id, you can easily change between test ids and this will expose what is checked on the page and what is not.

typescript
cy.get([data-testid="role-student-checked"]).should('be.visible');
cy.get([data-testid="role-teacher"]).click();
cy.get([data-testid="role-teacher-checked"]).should('be.visible');

As you can see this test will allow you to check what option is selected without having to hook into to any attributes that would be used for development. It is also readable and allows an engineer to quickly figure out what radio button is selected.

Extra credits

Co-location of source code and testing code allows there to be an abstraction of a test id framework away from the code.

Having non-static test ids allows testers to have more control over the code that they test. Aria-hidden is a great attribute but it is a development attribute and can sometimes happen asynchronously to the attribute on the page. Using a testing strategy like data-testid="test-title-hidden" or data-teststate="hidden" allows the automation script to know that a component should not be modifiable by an end-user.

As a QA developer, you should be confident and competent to add your own hooks to the code. Sometimes when developing a testing strategy you can't think of all the potential use cases during ideation. Add those to the code and make a note to add the use case to story-time discussions later.

If there are security issues around having automation hooks in your front end code you can use feature flagging or babel to pull out the hooks as they go into production. Though, this should be tested as well though with a post deployment test.