Best Practices¶
Guidelines for writing effective, maintainable tests with aria-testing.
Query Priority¶
Use queries in this order of preference:
1. By Role ⭐⭐⭐ (Highest Priority)¶
Find elements by their ARIA role - mirrors how screen readers interact with your app.
button = get_by_role(document, "button")
heading = get_by_role(document, "heading", level=1)
link = get_by_role(document, "link", name="Home")
Why prioritize role queries?
Tests how users with assistive technology experience your app
Encourages semantic HTML
Resilient to implementation changes (CSS classes, IDs, structure)
Forces you to think about accessibility
2. By Label Text ⭐⭐⭐¶
Find form elements by their associated label - how users identify form fields.
username = get_by_label_text(document, "Username")
email = get_by_label_text(document, "Email Address")
Why prioritize label queries?
Matches how users identify form inputs
Ensures labels are properly associated
Tests accessibility of forms
Resilient to implementation changes
3. By Text ⭐⭐¶
Find elements by their text content.
button = get_by_text(document, "Click me")
heading = get_by_text(document, "Welcome")
Why lower priority than role?
Text can change more frequently than roles
Doesn’t verify semantic structure
Less specific than role + name combination
When to use:
Content verification (headings, paragraphs)
When role queries aren’t specific enough
Testing displayed text
4. By Test ID ⭐¶
Find elements by data-testid attribute - escape hatch when semantic queries fail.
component = get_by_test_id(document, "user-menu")
Why lowest priority?
Implementation detail, not user-facing
Adds attributes solely for testing
Doesn’t encourage accessibility
Doesn’t match how users interact
When to use:
Complex components without clear roles
Dynamic content with changing text
When semantic queries are impractical
Testing implementation-specific behavior
5. By Tag Name, ID, Class¶
Direct attribute queries - use sparingly.
# Tag name: useful for non-semantic elements
favicon = get_by_tag_name(document, "link", attrs={"rel": "icon"})
# ID: when element has unique ID
element = get_by_id(document, "main-content")
# Class: when other queries fail
buttons = get_all_by_class(document, "btn-primary")
When to use:
Non-semantic HTML elements (
<link>,<meta>,<script>)Testing specific HTML structure
When semantic queries aren’t possible
Query Variant Selection¶
Choose the right variant for your use case:
get_by_* - Most Common¶
Use when element MUST exist and be unique:
# Element is required and unique
button = get_by_role(document, "button", name="Submit")
heading = get_by_text(document, "Welcome")
Fails fast:
Raises
ElementNotFoundErrorif not foundRaises
MultipleElementsErrorif multiple found
query_by_* - Conditional Checks¶
Use when checking if element exists or not:
# Check for conditional rendering
logout_button = query_by_role(document, "button", name="Logout")
if logout_button is None:
# User not logged in
login_link = get_by_role(document, "link", name="Login")
Returns None instead of raising when not found.
get_all_by_* - Multiple Required¶
Use when multiple elements MUST exist:
# Verify multiple items exist
links = get_all_by_role(nav, "link")
assert len(links) == 3
Fails fast:
Raises
ElementNotFoundErrorif none foundReturns all matches if any exist
query_all_by_* - Exploratory¶
Use when finding zero or more elements:
# Find all, might be empty
errors = query_all_by_role(form, "alert")
if errors:
# Handle errors
pass
Never raises - returns empty list if none found.
Accessibility Testing¶
Test What Users Experience¶
# ✅ Good - tests accessible name
button = get_by_role(document, "button", name="Close dialog")
# ❌ Bad - tests implementation detail
button = get_by_test_id(document, "close-btn")
Verify Semantic Structure¶
# ✅ Good - verifies heading hierarchy
h1 = get_by_role(document, "heading", level=1)
h2 = get_by_role(document, "heading", level=2)
# ❌ Bad - doesn't verify semantic meaning
h1 = get_by_tag_name(document, "h1")
Ensure Form Accessibility¶
# ✅ Good - verifies label association
email_input = get_by_label_text(form, "Email Address")
# ❌ Bad - doesn't verify label exists
email_input = get_by_tag_name(form, "input", attrs={"name": "email"})
Pattern Matching¶
Use regex for flexible matching:
import re
# Case-insensitive matching
link = get_by_role(document, "link", name=re.compile(r"home", re.IGNORECASE))
# Partial matching
heading = get_by_text(document, re.compile(r"Welcome.*"))
# Multiple options
button = get_by_role(document, "button", name=re.compile(r"Save|Submit"))
Error Handling¶
Let Tests Fail Fast¶
# ✅ Good - fails immediately with clear error
button = get_by_role(document, "button", name="Submit")
# ❌ Bad - returns None, test fails later with unclear error
button = query_by_role(document, "button", name="Submit")
assert button is not None # Less clear failure message
Use Descriptive Queries¶
# ✅ Good - error message includes "Submit"
button = get_by_role(document, "button", name="Submit")
# ❌ Bad - error message just says "button not found"
button = get_by_role(document, "button")
Handle Expected Absences¶
# ✅ Good - explicit about checking absence
logout = query_by_role(document, "button", name="Logout")
assert logout is None, "Should not show logout when not logged in"
# ❌ Bad - try/except hides the intent
try:
logout = get_by_role(document, "button", name="Logout")
assert False, "Should not find logout button"
except ElementNotFoundError:
pass # Expected
Performance Tips¶
Reuse Containers¶
Cache benefits from repeated queries on the same container:
# ✅ Good - queries same document, benefits from cache
def test_page():
document = render_page()
nav = get_by_role(document, "navigation")
main = get_by_role(document, "main")
footer = get_by_role(document, "contentinfo")
# ❌ Less efficient - re-renders between queries
def test_page():
nav = get_by_role(render_page(), "navigation")
main = get_by_role(render_page(), "main") # New document, cold cache
Scope Queries Appropriately¶
Query from the smallest container that includes your target:
# ✅ Good - scoped to specific section
form = get_by_role(document, "form")
submit = get_by_role(form, "button", name="Submit") # Only searches form
# ❌ Less efficient - searches entire document
submit = get_by_role(document, "button", name="Submit")
Use query_all_* for Multiple Queries¶
Full caching benefits when finding all matches:
# ✅ Good - single query gets all
links = query_all_by_role(nav, "link")
home_link = next(l for l in links if "Home" in get_text_content(l))
# ❌ Less efficient - multiple queries
home_link = get_by_role(nav, "link", name="Home")
docs_link = get_by_role(nav, "link", name="Docs")
Test Organization¶
Separate Structure and Content¶
def test_form_structure():
"""Test form has correct semantic structure."""
form = get_by_role(document, "form")
get_by_label_text(form, "Username")
get_by_label_text(form, "Password")
get_by_role(form, "button", name="Login")
def test_form_behavior():
"""Test form submission behavior."""
# Test behavior separately from structure
pass
Common Pitfalls¶
Don’t Test Implementation Details¶
# ❌ Bad - tests CSS classes
buttons = get_all_by_class(document, "btn-primary")
# ✅ Good - tests accessible interface
buttons = get_all_by_role(document, "button")
Don’t Over-Specify Queries¶
# ❌ Bad - overly specific, brittle
button = get_by_tag_name(
document,
"button",
attrs={"class": "btn btn-primary", "id": "submit-btn"}
)
# ✅ Good - specific enough, flexible
button = get_by_role(document, "button", name="Submit")
Don’t Mix Query Strategies¶
# ❌ Bad - inconsistent strategy
nav = get_by_role(document, "navigation")
links = get_all_by_tag_name(nav, "a") # Why switch to tag name?
# ✅ Good - consistent use of roles
nav = get_by_role(document, "navigation")
links = get_all_by_role(nav, "link")
Don’t Ignore Accessibility Failures¶
# ❌ Bad - falls back to test ID without fixing accessibility
try:
button = get_by_role(document, "button", name="Submit")
except ElementNotFoundError:
button = get_by_test_id(document, "submit-btn") # Hides accessibility issue
# ✅ Good - let test fail, fix the component
button = get_by_role(document, "button", name="Submit")
Testing Non-Semantic Elements¶
Some elements don’t have ARIA roles and should use tag queries:
# ✅ Good - use tag queries for <head> elements
stylesheets = get_all_by_tag_name(head, "link", attrs={"rel": "stylesheet"})
favicon = get_by_tag_name(head, "link", attrs={"rel": "icon"})
viewport = get_by_tag_name(head, "meta", attrs={"name": "viewport"})
# ✅ Good - use tag queries for scripts
scripts = get_all_by_tag_name(document, "script")
Summary Checklist¶
[ ] Prefer role queries over other query types
[ ] Use label queries for form inputs
[ ] Use
get_by_*for required unique elements[ ] Use
query_by_*for conditional checks[ ] Let tests fail fast with clear error messages
[ ] Test accessible names and semantic structure
[ ] Avoid testing implementation details (classes, IDs, structure)
[ ] Reuse containers to benefit from caching
[ ] Use descriptive query parameters
[ ] Handle expected absences explicitly with
query_by_*
See Also¶
Query Reference - All available query functions
Examples - Real-world testing patterns
API Reference - Complete function signatures