April 16, 2024
Using data-testid in React Cypress tests
In React applications, using data-testid
with Cypress makes it easy to select elements for testing. This approach is ideal when UI components change frequently but need consistent functionality. By using a unique data-testid
for each element, tests are simpler to maintain and more stable, as these identifiers allow straightforward access to components, regardless of visual changes.
Why data-testid?
Since they look the same, why should would we use data-testid
over id
?
// Using data-testid
function LoginButton() {
return <button data-testid="login-button">Log In</button>;
}
// Using id
function LoginButton() {
return <button id="login-button">Log In</button>;
}
data-testid
indicates its use for testing, which helps it stand out from other attributes like id or class that might be used for styling or scripting.- Since
data-testid
is solely for testing, your tests remain unaffected by changes to styles or scripts that use id or class for other purposes. data-testid
can be safely removed in production. This keeps the production code clean and optimized, while also preventing easy scraping.
A large benefit is also the improved readability. With data-testid
, you can use descriptive names that reflect the element's role. Additionally, this can be added to more elements since it doesn't affect the styles.
cy.get('[data-testid="login-button"]').click();
For a comparison of different selectors, the Cypress documentation provides a table which can be found here. But in summary, data-testid
offers the best benefit, allowing full isolation from testing and production.
Example
Let's take a look at a practical example.
Suppose we had this test that relies on id selectors:
it("Should reset password", () => {
cy.visit("http://localhost:3000/forgot");
cy.get("#email").type(testEmailAddress);
cy.get("form").submit();
// this will send an email with a reset link to the provided email address
cy.mailiskSearchInbox(Cypress.env("MAILISK_NAMESPACE"), {
to_addr_prefix: testEmailAddress,
subject_includes: "reset",
timeout: 1000 * 60,
}).then((response) => {
const emails = response.data;
const email = emails[0];
resetLink = email.text.match(/.*\[(http:\/\/localhost:3000\/.*)\].*/)[1];
expect(resetLink).to.not.be.undefined;
});
});
If we change the component a bit and add our data-testid
attribute:
return (
<div className="flex justify-center mt-9">
<div className="bg-gray-100 rounded-md">
<form data-testid="password-form" ...>
<p>We'll send a password reset link</p>
<input
type="text"
id="email"
data-testid="email-input"
...
/>
...
</form>
</div>
</div>
);
We can then use these data-testid
attributes directly instead:
it("Should reset password", () => {
cy.visit("http://localhost:3000/forgot");
cy.get('[data-testid="email-input"]').type(testEmailAddress);
cy.get('[data-testid="password-form"]').submit();
...
});
Removing data-testid
from production
This is dependent on your project tooling and frameworks. There's a plugin for every popular tool that allows the removal of custom attributes (data-testid is an attribute, after all).
For example, let's say you're using Babel and JSX components (React). In that case, something like the babel-plugin-jsx-remove-data-test-id library will fit your needs.
No matter what you choose, make sure to look at the documentation and select the correct attribute. In this post, we mentioned data-testid
, but this is by no means a standard, so adjust accordingly to your project.