Writing resilient tests: Query Selection and Debugging
Summarize
Summary of Writing resilient tests: Query Selection and Debugging
This guidance emphasizes building resilient automated tests in ServiceNow by querying UI components based on how users interact with them, using ARIA roles and accessible names, rather than relying on fragile implementation details like CSS selectors or element IDs. It highlights the importance of semantic queries for long-term test stability, especially through UI refactoring and asynchronous content loading.
Show less
Building Resilient Test Queries
- Use semantic queries such as getByRole and getByLabelText as primary selectors. These queries reflect the actual user experience and accessibility semantics.
- Avoid selectors tied to internal implementation details (e.g., element IDs, generated class names, nested CSS paths), as these frequently change and cause test failures.
- If no accessible query fits the need, resort to getBySelector but document why accessible queries are insufficient.
- Semantic queries have proven to be resilient even when underlying DOM structures are heavily refactored.
Handling Asynchronous Content
- Modern interfaces often load content asynchronously after navigation or API calls.
- Use findBy queries (e.g., findByRole) that poll the DOM until elements appear or timeout, avoiding immediate failures seen with getBy queries.
Debugging Failed Queries
- Use screen.debug() to output the live DOM in the browser console, including shadow DOM, to see the actual markup tested against your queries.
- Use snatf.evaluate(() => ...) to execute JavaScript inside the test iframe, allowing inspection of runtime component state and computed values.
- For asynchronous failures, wrap assertions in waitFor with extended timeouts, monitor console logs for intermediate states, and insert screen.debug() calls to isolate timing or loading issues.
Key Outcomes
- Tests become more maintainable and less prone to breakage during UI refactoring or redesign.
- Better alignment with user experience and accessibility standards ensures tests validate meaningful behavior.
- Enhanced debugging tools and strategies improve test reliability and speed up failure diagnosis.
Resilient automated tests are built by querying components semantically, through ARIA roles and accessible names, rather than relying on implementation details that teams can change at any time.
Building Resilient Test Queries
The foundation of reliable automated testing rests on a single principle: query your components the way users interact with them, not the way they're built internally.
When you select elements using ARIA roles and accessible names, through queries like getByRole and getByLabelText, your tests reflect the semantic contract of the component. This approach
proved invaluable in a recent case where the SoW team restructured the DOM of a critical component. Tests written against role-based queries continued to pass seamlessly, while tests relying on CSS selectors and internal
element IDs shattered immediately. The difference was striking: semantic queries stayed resilient through refactoring, while implementation-dependent queries became brittle.
- Start with
getByRoleandgetByLabelTextas your primary tools. These queries prioritize accessibility and semantic meaning, ensuring your tests validate what users actually experience. - Only reach for
getBySelectorwhen no accessible query satisfies your needs. Document why the accessible option wasn't viable. - Avoid anchoring tests to element IDs, generated class names, or deeply nested CSS paths. Component teams can refactor these implementation details without warning, leaving your tests fragile and your test suite unmaintainable.
Handling Asynchronous Content
Automated tests must account for the reality of modern interfaces: content doesn't always appear immediately. After navigation events, API calls, or animations complete, elements enter the DOM asynchronously. In these cases,
findBy* queries (such as findByRole) are essential. They poll the DOM at intervals until the element appears or the timeout expires—a far more reliable approach than
getBy*, which throws an exception immediately if the element isn't found on the first check.
Debugging Failed Queries
When a query doesn't match what you expected, start with screen.debug(). This method outputs the complete live DOM of the tested page directly to your browser console, including shadow root contents. You're seeing the actual markup your query runs against, an invaluable reference point for diagnosis.
For scenarios where you need to inspect computed values or execute code in the page context, use sn_atf.evaluate(() => ...). This function runs arbitrary JavaScript inside the tested iframe and returns the
result, giving you direct access to component state and runtime values.
When async failures occur, wrap your assertion in waitFor with an extended timeout and monitor the console for intermediate states. This technique isolates timing issues and reveals whether content is
appearing later than expected or not appearing at all. Combined with screen.debug() calls at key moments, you can identify the root cause and adjust your test accordingly.