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.

Ready to start testing emails?
Create a free account.

Get started