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" />
β 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 })
2. Naming Patterns & Recommendations
πΉ Pattern 1: Component-Scoped Kebab-Case (β Recommended)
Format: {component}-{element}-{type}
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"
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}
data-testid="auth.login-form.submit-btn" data-testid="ecommerce.product-grid.filter-btn" data-testid="admin.user-table.delete-btn"
Pros: clear domain separation, good for micro-frontends.
Cons: tooling issues with dots, harder refactors.
πΉ Pattern 3: BEM-Style
Format: {block}__{element}--{modifier}
data-testid="product-card__image--loading" data-testid="nav-menu__item--active" data-testid="form-input__field--error"
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
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;
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;
// 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> );
5. Conflict Prevention Strategies
- Namespace by Domain
data-testid="auth-login-form-submit-btn" data-testid="ecommerce-product-card-add-btn"
- Component-Level Scoping
export const USER_PROFILE_TEST_IDS = { avatar: 'user-profile-avatar', editBtn: 'user-profile-edit-btn', };
- Scoped Helpers
const getTestId = (scope: string, element: string) => `${scope}-${element}`; data-testid={getTestId('user-profile', 'avatar')} data-testid={`cart-item-${item.id}-remove-btn`}
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
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]+)*", }], }, }, ];
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; }, };
Utility Function
export const testId = (id: string) => process.env.NODE_ENV !== "production" ? { "data-testid": id } : {}; // Usage <button {...testId("login-form-submit-btn")}>Submit</button>
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)