Creating Reusable Test IDs with BEM Methodology
Learn how to create maintainable and reusable test IDs using BEM naming conventions for robust and reliable testing. ~ Quido Hoekman
Table of Contents
- The Importance of Reliable Test IDs
- What Are Test IDs?
- Why BEM for Test IDs?
- BEM Test ID Structure
- Practical Examples
- Component Design with Hierarchical Test IDs
- Alternative Approach: Using
within()for Component Scoping - Standardizing Test IDs
- Best Practices
- Conclusion
- Resources and References
The Importance of Reliable Test IDs
In modern web development, automated testing is crucial for maintaining code quality and preventing regressions. However, tests that rely on CSS selectors, class names, or DOM structure are fragile and break easily when the UI changes. This is where test IDs come in—they provide a stable, semantic way to identify elements for testing purposes.
What Are Test IDs?
Test IDs are special attributes used to identify elements in the DOM specifically for testing. They should be implemented using the data-testid attribute, which clearly indicates their purpose and separates them from other data attributes.
<button data-testid="submit-button">Submit</button>
<div data-testid="user-profile">...</div>
Why BEM for Test IDs?
BEM (Block Element Modifier) is a CSS naming methodology that provides a clear, consistent structure for naming. When applied to test IDs, BEM offers several advantages:
- Consistency - Predictable naming patterns across your entire application
- Scalability - Easy to extend and maintain as your application grows
- Clarity - Self-documenting names that clearly indicate component relationships
- Team Alignment - Everyone follows the same naming conventions
BEM Test ID Structure
The BEM methodology for test IDs follows this pattern:
[component-name]__[sub-component-name]__[item-name]-[item-id]__[feature]--[condition]
Single Instance Components
For components that appear only once on a page, use simple BEM naming:
<!-- Card component -->
<div data-testid="card">
<h2 data-testid="card__title">Product Name</h2>
<p data-testid="card__description">Product description</p>
<button data-testid="card__button">Add to Cart</button>
</div>
Multiple Instance Components
For components that appear multiple times (like list items), include a unique identifier:
<!-- List component with multiple items -->
<ul data-testid="list">
<li data-testid="list__item-1">First item</li>
<li data-testid="list__item-2">Second item</li>
<li data-testid="list__item-3">Third item</li>
</ul>
Practical Examples
Navigation Component
<nav data-testid="navigation">
<ul data-testid="navigation__menu">
<li data-testid="navigation__item-home">
<a data-testid="navigation__link-home" href="/">Home</a>
</li>
<li data-testid="navigation__item-about">
<a data-testid="navigation__link-about" href="/about">About</a>
</li>
</ul>
<button data-testid="navigation__toggle">Menu</button>
</nav>
Form Component
<form data-testid="contact-form">
<div data-testid="contact-form__field-group">
<label data-testid="contact-form__label-name">Name</label>
<input data-testid="contact-form__input-name" type="text" />
</div>
<div data-testid="contact-form__field-group">
<label data-testid="contact-form__label-email">Email</label>
<input data-testid="contact-form__input-email" type="email" />
</div>
<button data-testid="contact-form__submit">Send Message</button>
</form>
Component Design with Hierarchical Test IDs
One powerful approach to test ID design is leveraging parent data-testid attributes to create hierarchical specificity. This allows sub-components to inherit context from their parent components, making test IDs more descriptive and maintainable.
Parent-Child Test ID Relationships
When designing components, consider how child components can leverage their parent’s test ID for increased specificity:
<!-- Parent component with specific context -->
<div data-testid="card__pricing-low">
<h3 data-testid="card__pricing-low__title">Basic Plan</h3>
<p data-testid="card__pricing-low__description">Perfect for individuals</p>
<div data-testid="card__pricing-low__features">
<span data-testid="card__pricing-low__feature-1">5 Projects</span>
<span data-testid="card__pricing-low__feature-2">1GB Storage</span>
</div>
<button data-testid="card__pricing-low__cta">Choose Plan</button>
</div>
Alternative Approach: Using within() for Component Scoping
Sometimes the test ID specificity isn’t enough, so we can use the within() function to scope the test assertions to a specific component. This is alternative if the hierarchical test ids are hard to implement.
Standardizing Test IDs
To ensure consistency across your application, it’s beneficial to create utility functions that automatically format and standardize test IDs. This approach helps maintain BEM conventions while reducing human error and ensuring uniform formatting.
Creating a Test ID Utility
Here’s a example of a vanilla JavaScript implementation:
/**
* TestIdFormatter class
* @class
* @description Transforms input values into standardized test IDs
*/
class TestIdFormatter {
/**
* Transforms input values into standardized test IDs
* @param {string|number|Array} value - The value(s) to transform
* @returns {string} - Formatted test ID
*/
transform(value) {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string" || typeof value === "number") {
return this.formatValue(value.toString());
}
if (Array.isArray(value)) {
return value
.map((item) => {
if (item === null || item === undefined) {
return "";
}
return this.formatValue(item.toString());
})
.join("__")
.toLowerCase();
}
return "";
}
/**
* Formats individual values for test ID usage
* @param {string} value - The value to format
* @returns {string} - Formatted value
*/
formatValue(value) {
return value
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/[\(\[\{]/g, "--") // Replace opening brackets with double hyphens
.replace(/[\)\]\}]/g, "") // Remove closing brackets
.replace(/[^a-zA-Z0-9-_]/g, ""); // Remove special characters except hyphens and underscores
}
}
// Usage examples
const testIdFormatter = new TestIdFormatter();
// Simple string formatting
console.log(testIdFormatter.transform("User Profile")); // "user-profile"
console.log(testIdFormatter.transform("Product (Premium)")); // "product--premium"
// Array formatting for hierarchical IDs
console.log(testIdFormatter.transform(["card", "pricing", "low"])); // "card__pricing__low"
console.log(testIdFormatter.transform(["user", "profile", "edit-button"])); // "user__profile__edit-button"
// Mixed types
console.log(testIdFormatter.transform(["product", "card", 123])); // "product__card__123"
Conclusion
Implementing BEM methodology for test IDs creates a robust foundation for automated testing. By following consistent naming conventions, you’ll build tests that are easier to maintain, understand, and extend. Start implementing these patterns in your next project, and you’ll see the benefits in both test reliability and team productivity.
Remember: good test IDs are an investment in your application’s long-term maintainability and your team’s development velocity.
Resources and References
- BEM - Block Element Modifier - Official BEM documentation guide
- BEM 101 - CSS-Tricks comprehensive guide to BEM naming conventions
- BEM Naming Convention - Detailed naming conventions and examples
- Testing Library - Queries - Best practices for querying elements in tests
- Playwright - Best Practices - Official best practices guide
- Cypress - Best Practices - Testing best practices and recommendations