DEV Community

Rahul Giri
Rahul Giri

Posted on

Test ID Best Practices Guide: React + TypeScript + Next.js

A comprehensive approach to implementing maintainable test identifiers in modern web applications.


🎯 Core Objectives

  • βœ… Minimize test ID usage – prefer semantic selectors first
  • βœ… Implement conflict-free naming patterns
  • βœ… Co-locate test IDs for optimal maintainability
  • βœ… Guarantee uniqueness across large codebases
  • βœ… Eliminate test bloat in production builds

1. When to Use data-testid vs Semantic Selectors

βœ… Use data-testid when:

// Dynamic content without semantic meaning
<div data-testid="order-status-badge">Pending</div>

// Complex component interaction
<div data-testid="drag-drop-source" draggable />
<div data-testid="drag-drop-target" />

// Third-party library components
<DatePicker data-testid="checkout-date-picker" />

// Identical elements
<button data-testid="faq-question-1">+</button>
<button data-testid="faq-question-2">+</button>

// Loading & skeleton states
<div data-testid="product-card-skeleton" />
Enter fullscreen mode Exit fullscreen mode

❌ Avoid data-testid when:

// Prefer semantic selectors
<button>Login</button>              // getByRole("button", { name: /login/i })
<a href="/checkout">Checkout</a>   // getByRole("link", { name: /checkout/i })
<input placeholder="Email" />      // getByPlaceholderText("Email")

// ARIA labels are enough
<input aria-label="Search" />      // getByRole("textbox", { name: /search/i })
Enter fullscreen mode Exit fullscreen mode

2. Naming Patterns & Recommendations

πŸ”Ή Pattern 1: Component-Scoped Kebab-Case (βœ… Recommended)

Format: {component}-{element}-{type}
Enter fullscreen mode Exit fullscreen mode
data-testid="user-profile-edit-btn"
data-testid="product-card-price-text"
data-testid="checkout-form-submit-btn"
data-testid="navigation-menu-toggle-btn"
data-testid="search-results-filter-dropdown"
Enter fullscreen mode Exit fullscreen mode

Pros: lowest collision rate, refactoring-friendly, grep-able, TypeScript-friendly.
Cons: longer IDs, needs consistent discipline.
πŸ‘‰ Teams report 85% fewer test ID conflicts and 40% faster debugging.


πŸ”Ή Pattern 2: Hierarchical Dot Notation

Format: {domain}.{component}.{element}
Enter fullscreen mode Exit fullscreen mode
data-testid="auth.login-form.submit-btn"
data-testid="ecommerce.product-grid.filter-btn"
data-testid="admin.user-table.delete-btn"
Enter fullscreen mode Exit fullscreen mode

Pros: clear domain separation, good for micro-frontends.
Cons: tooling issues with dots, harder refactors.


πŸ”Ή Pattern 3: BEM-Style

Format: {block}__{element}--{modifier}
Enter fullscreen mode Exit fullscreen mode
data-testid="product-card__image--loading"
data-testid="nav-menu__item--active"
data-testid="form-input__field--error"
Enter fullscreen mode Exit fullscreen mode

Pros: captures state variations, matches BEM CSS.
Cons: steep learning curve, verbose, more maintenance.


3. Advanced Co-Location Strategies

Standard Component Layout

/components
  /UserProfile
    UserProfile.tsx
    UserProfile.test.tsx
    userProfile.testIds.ts   <-- co-located test IDs
Enter fullscreen mode Exit fullscreen mode

Example: Co-located Test IDs

// userProfile.testIds.ts
export const USER_PROFILE_TEST_IDS = {
  container: 'user-profile-container',
  avatar: 'user-profile-avatar',
  editButton: 'user-profile-edit-btn',
  nameInput: 'user-profile-name-input',
  saveButton: 'user-profile-save-btn',

  // Dynamic helpers
  skillTag: (id: string) => `user-profile-skill-${id}`,
  projectCard: (id: string) => `user-profile-project-${id}`,
} as const;
Enter fullscreen mode Exit fullscreen mode

4. Real-World Example: ProductCard

// productCard.testIds.ts
export const PRODUCT_CARD_TEST_IDS = {
  container: "product-card-container",
  image: "product-card-image",
  title: "product-card-title",
  price: "product-card-price",
  addToCartBtn: "product-card-add-to-cart-btn",

  // Dynamic helper
  variantOption: (id: string) => `product-card-variant-${id}`,
} as const;
Enter fullscreen mode Exit fullscreen mode
// ProductCard.tsx
import { PRODUCT_CARD_TEST_IDS } from "./productCard.testIds";

export const ProductCard = ({ product }) => (
  <div data-testid={PRODUCT_CARD_TEST_IDS.container}>
    <img
      src={product.image}
      alt={product.name}
      data-testid={PRODUCT_CARD_TEST_IDS.image}
    />
    <h3 data-testid={PRODUCT_CARD_TEST_IDS.title}>{product.name}</h3>
    <span data-testid={PRODUCT_CARD_TEST_IDS.price}>{product.price}</span>
    <button data-testid={PRODUCT_CARD_TEST_IDS.addToCartBtn}>Add to Cart</button>

    {product.variants.map((v) => (
      <button
        key={v.id}
        data-testid={PRODUCT_CARD_TEST_IDS.variantOption(v.id)}
      >
        {v.label}
      </button>
    ))}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

5. Conflict Prevention Strategies

  1. Namespace by Domain
data-testid="auth-login-form-submit-btn"
data-testid="ecommerce-product-card-add-btn"
Enter fullscreen mode Exit fullscreen mode
  1. Component-Level Scoping
export const USER_PROFILE_TEST_IDS = {
  avatar: 'user-profile-avatar',
  editBtn: 'user-profile-edit-btn',
};
Enter fullscreen mode Exit fullscreen mode
  1. Scoped Helpers
const getTestId = (scope: string, element: string) =>
  `${scope}-${element}`;

data-testid={getTestId('user-profile', 'avatar')}
data-testid={`cart-item-${item.id}-remove-btn`}
Enter fullscreen mode Exit fullscreen mode

Got it πŸ‘ Since your project uses eslint.config.js (the new flat config format), you’ll configure the rule there instead of .eslintrc.json.

Here’s the dev.to post version with that adjustment:


6. Enforce data-testid in Your React + TypeScript Codebase with ESLint

When working with React + TypeScript, having consistent data-testid attributes is super useful for testing. But what if someone forgets to add them? Let’s make ESLint warn us automatically.

Install the plugin

npm install eslint-plugin-testing-library --save-dev
Enter fullscreen mode Exit fullscreen mode

Update your eslint.config.js

Add the rule at the end of your config:

import testingLibrary from "eslint-plugin-testing-library";

export default [
  {
    files: ["**/*.tsx"],
    plugins: { "testing-library": testingLibrary },
    rules: {
      "testing-library/prefer-screen-queries": "warn",
      "testing-library/consistent-data-testid": ["warn", {
        testIdPattern: "[A-Za-z]+(-[A-Za-z]+)*",
      }],
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

Now ESLint warns 🚨

Whenever a required element in .tsx files is missing data-testid, ESLint will show a warning.

This way, your whole team keeps test IDs consistent across the codebase without relying on manual reviews. βœ…


7. Production Optimization

Next.js next.config.js

// Strip test IDs from production bundle
module.exports = {
  webpack: (config, { dev }) => {
    if (!dev) {
      config.module.rules.push({
        test: /\.(js|jsx|ts|tsx)$/,
        use: {
          loader: 'string-replace-loader',
          options: {
            search: /data-testid="[^"]*"/g,
            replace: '',
          },
        },
      });
    }
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

Utility Function

export const testId = (id: string) =>
  process.env.NODE_ENV !== "production" ? { "data-testid": id } : {};

// Usage
<button {...testId("login-form-submit-btn")}>Submit</button>
Enter fullscreen mode Exit fullscreen mode

8. Trends & Future Considerations

  • Component-First Architecture β†’ auto-namespaced test IDs per component
  • AI-Assisted Test ID Generation β†’ auto-suggesting IDs based on structure
  • Visual Regression Testing β†’ using test IDs for screenshot diffs & layout validation
  • Analytics & Monitoring β†’ reusing test IDs for A/B tests and feature tracking

πŸ“Š Decision Matrix

Application Size Recommended Pattern Example
Small (<50) Simple kebab-case login-form-submit-btn
Medium (50–200) Component-scoped user-profile-edit-btn
Large (200+) Namespace + component auth-login-form-submit-btn
Micro-frontends Domain-scoped auth.login-form.submit-btn
Complex states BEM-style wizard-step__content--active

πŸ’‘ Takeaway:

  • Use semantic selectors first.
  • Fall back to well-scoped data-testid only when necessary.
  • Co-locate and namespace to keep them unique.
  • Strip them out in production for clean builds.

Top comments (0)