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)