Search⌘ K
AI Features

Dom Elements Selection: data-testid

Explore how to select stable DOM elements for end-to-end tests using the data-testid attribute. Understand why common selectors can cause fragile tests and learn to implement dedicated attributes that improve test reliability and error diagnosis.

We'll cover the following...

data-testid

The following is a deterministic event test:

C++
context("Signup flow", () => {
it("The happy path should work", () => {
cy.visit("/register");
cy.get(".form-control").then($els => {
const random = Math.floor(Math.random() * 100000);
cy.get($els[0]).type(`Tester${random}`);
cy.get($els[1]).type(`user+${random}@realworld.io`);
cy.get($els[2]).type("mysupersecretpassword");
});
cy.get("button").click();
cy.contains("No articles are here", { timeout: 10000 }).should("be.visible");
});
});

As we’ve seen, one of the defects of the above test is its uselessness while something goes wrong while retrieving the elements.

The HTML of the RealWorld form is as follows:

C++
<form>
<fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="text"
placeholder="Username"
value=""
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="email"
placeholder="Email"
value=""
/>
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
value=""
/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" type="submit">
Sign up
</button>
</fieldset>
</form>

If we want to avoid using the order of elements for the foundation of our test, we can leverage:

  • The type: text, email, and password

  • The placeholder

The test could become the following (based on the type)

C++
context("Signup flow", () => {
it("The happy path should work", () => {
cy.visit("/register");
- cy.get(".form-control").then($els => {
- const random = Math.floor(Math.random() * 100000);
- cy.get($els[0]).type(`Tester${random}`);
- cy.get($els[1]).type(`user+${random}@realworld.io`);
- cy.get($els[2]).type("mysupersecretpassword");
- });
+ const random = Math.floor(Math.random() * 100000);
+ cy.get("[type=text]").type(`Tester${random}`);
+ cy.get("[type=email]").type(`user+${random}@realworld.io`);
+ cy.get("[type=password]").type("mysupersecretpassword");
cy.get("button").click();
cy.contains("No articles are here", { timeout: 10000 }).should("be.visible");
});
});

or, leveraging the placeholder

C++
context("Signup flow", () => {
it("The happy path should work", () => {
cy.visit("/register");
- cy.get(".form-control").then($els => {
- const random = Math.floor(Math.random() * 100000);
- cy.get($els[0]).type(`Tester${random}`);
- cy.get($els[1]).type(`user+${random}@realworld.io`);
- cy.get($els[2]).type("mysupersecretpassword");
- });
+ const random = Math.floor(Math.random() * 100000);
+ cy.get("[placeholder=Username]").type(`Tester${random}`);
+ cy.get("[placeholder=Email]").type(`user+${random}@realworld.io`);
+ cy.get("[placeholder=Password]").type("mysupersecretpassword");
cy.get("button").click();
cy.contains("No articles are here", { timeout: 10000 }).should("be.visible");
});
});

When everything is normal, both the type and placeholder work as expected. But if the email input field is not rendered correctly, they also help us to understand what is wrong.

What not to do

There are a ton of selectors we can use when finding DOM elements, but most of them could change because of different purposes (and then break the test inadvertently):

Selector Purpose
classes Style
ids JavaScript
HTML tags standardization/SEO
type User experience
ARIA attributes User experience

These selectors do not have testing purposes. So, the more the tests use them, the more fragile these tests become. They create problems because of class changes.

You need to use a dedicated attribute that will not change with other needs.This attribute is data-testid (or data-test, or data-cy, etc.).

The markup of the form must be changed with the new attribute.

C++
<form>
<fieldset>
<fieldset class="form-group">
- <input class="form-control form-control-lg" type="text" placeholder="Username" value="" />
+ <input class="form-control form-control-lg" type="text" placeholder="Username" value="" data-testid="username"/>
</fieldset>
<fieldset class="form-group">
- <input class="form-control form-control-lg" type="email" placeholder="Email" value="" />
+ <input class="form-control form-control-lg" type="email" placeholder="Email" value="" data-testid="email"/>
</fieldset>
<fieldset class="form-group">
- <input class="form-control form-control-lg" type="password" placeholder="Password" value="" />
+ <input class="form-control form-control-lg" type="password" placeholder="Password" value="" data-testid="password"/>
</fieldset>
- <button class="btn btn-lg btn-primary pull-xs-right" type="submit">Sign up</button>
+ <button class="btn btn-lg btn-primary pull-xs-right" type="submit" data-testid="signup-button">Sign up</button>
</fieldset>
</form>

The final form is taken from lines 73-119 in the Register.js file.

C++
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import agent from '../agent';
import { REGISTER, REGISTER_PAGE_UNLOADED, UPDATE_FIELD_AUTH } from '../constants/actionTypes';
import ListErrors from './ListErrors';
export const strings = {
username: "Username",
email: "Email",
password: "Password",
signUp: "Sign up",
}
const mapStateToProps = state => ({ ...state.auth });
const mapDispatchToProps = dispatch => ({
onChangeEmail: value =>
dispatch({ type: UPDATE_FIELD_AUTH, key: 'email', value }),
onChangePassword: value =>
dispatch({ type: UPDATE_FIELD_AUTH, key: 'password', value }),
onChangeUsername: value =>
dispatch({ type: UPDATE_FIELD_AUTH, key: 'username', value }),
onSubmit: (username, email, password) => {
const payload = agent.Auth.register(username, email, password);
dispatch({ type: REGISTER, payload })
},
onUnload: () =>
dispatch({ type: REGISTER_PAGE_UNLOADED })
});
class Register extends React.Component {
constructor() {
super();
this.changeEmail = ev => this.props.onChangeEmail(ev.target.value);
this.changePassword = ev => this.props.onChangePassword(ev.target.value);
this.changeUsername = ev => this.props.onChangeUsername(ev.target.value);
this.submitForm = (username, email, password) => ev => {
ev.preventDefault();
this.props.onSubmit(username, email, password);
}
if(window.Cypress) {
window.appActions = window.appActions || {};
window.appActions.signup = ({username, email, password}) => this.props.onSubmit(username, email, password);
}
}
componentWillUnmount() {
this.props.onUnload();
}
render() {
const email = this.props.email;
const password = this.props.password;
const username = this.props.username;
return (
<div className="auth-page">
<div className="container page">
<div className="row">
<div className="col-md-6 offset-md-3 col-xs-12">
<h1 className="text-xs-center">Sign Up</h1>
<p className="text-xs-center">
<Link to="/login">
Have an account?
</Link>
</p>
<ListErrors errors={this.props.errors} />
<form onSubmit={this.submitForm(username, email, password)}>
<fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="text"
placeholder={strings.username}
value={this.props.username}
onChange={this.changeUsername}
data-testid="username"
/>
</fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="email"
placeholder={strings.email}
value={this.props.email}
onChange={this.changeEmail}
data-testid="email"
/>
</fieldset>
<fieldset className="form-group">
<input
className="form-control form-control-lg"
type="password"
placeholder={strings.password}
value={this.props.password}
onChange={this.changePassword}
data-testid="password"
/>
</fieldset>
<button
className="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled={this.props.inProgress}
data-testid="signup-button"
>
{strings.signUp}
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Register);

And the test is:

C++
context("Signup flow", () => {
it("The happy path should work", () => {
cy.visit("/register");
const random = Math.floor(Math.random() * 100000);
- cy.get("[placeholder=Username]").type(`Tester${random}`);
- cy.get("[placeholder=Email]").type(`user+${random}@realworld.io`);
- cy.get("[placeholder=Password]").type("mysupersecretpassword");
- cy.get("button").click();
- cy.contains("No articles are here", { timeout: 10000 }).should("be.visible");
+ cy.get("[data-testid=username]").type(`Tester${random}`);
+ cy.get("[data-testid=email]").type(`user+${random}@realworld.io`);
+ cy.get("[data-testid=password]").type("mysupersecretpassword");
+ cy.get("[data-testid=signup-button]").click();
+ cy.get("[data-testid=no-articles-here]", { timeout: 10000 }).should("be.visible");
});
});

The test leverages dedicated attributes and selectors that do not change for non-testing purposes. The feedback in case of failure is comparable to the previous successful test.

The complete test is as follows:

Note: You can see the Cypress UI better by opening the link next to Your app can be found at:

context("Signup flow", () => {
  it("The happy path should work", () => {
    cy.visit("/register");
    const random = Math.floor(Math.random() * 100000);
    cy.get("[data-testid=username]").type(`Tester${random}`);
    cy.get("[data-testid=email]").type(`user+${random}@realworld.io`);
    cy.get("[data-testid=password]").type("mysupersecretpassword");
    cy.get("[data-testid=signup-button]").click();
    cy.get("[data-testid=no-articles-here]", { timeout: 10000 }).should("be.visible");
  });
});
Final test

Unlike the previous version, this test helps us identify precisely what does not work, in case of failures related to the DOM elements.