In a test suite running for a year or longer, a few tests keep passing without anyone touching them. They are not merely easy. Their construction makes them resilient: the locator still works after a button moves, and the assertion ignores implementation details that change often. Each test checks a real behavior, then continues checking it correctly over time.
These tests are deliberate, not lucky. Durable tests share consistent properties that teams can learn. Knowing those properties helps more than another coverage metric or framework recommendation.
The locator problem
Most failures without an underlying code bug come from locators: CSS selectors, XPath expressions, or element queries used to find things on a page. A test locating a button by CSS class breaks when a style refactor renames that class. One locating an element by its DOM position breaks when another element is inserted above it. Neither failure says anything about application behavior. They only show that the test's UI map no longer matches the UI.
Tests that keep passing use locators based on meaning rather than presentation. In Playwright, that means using the accessibility tree: page.getByRole('button', { name: 'Submit order' }) survives styling changes or DOM restructuring when the button keeps its accessible role and label. The test asks, "does a submit button exist and can I interact with it," which is the behavioral question that matters.
When accessible selectors are unavailable, such as in legacy applications or poorly implemented component libraries, data-testid is a sensible fallback. The locator strategy should stay separate from visual design choices. A designer changing the CSS class prefix should not create a test concern.
One behavior per test
Tests that do too much fail in ways that are difficult to diagnose. A test that signs in, opens settings, changes a profile field, and checks a confirmation email can fail at any of five steps. In CI, the developer must rerun it locally, reproduce the issue, and follow the sequence to locate the break.
Tests that last tend to stay narrow. Each isolates one behavior. Setup is minimal and explicit: create the needed state, perform the action under test, and assert the expected outcome. If another behavior needs checking, give it a separate test and setup.
This narrow scope also makes failures useful. A test named checkout flow submits correctly on payment success that fails during a deploy points to a specific break. A test named full user journey could have failed for almost any reason.
Assertions that check behavior, not state
A test asserting internal application state is tightly coupled to implementation. One asserting observable behavior is not.
The practical difference is clear: checking that a button has the CSS class active after a click depends on how activation is represented in CSS. Checking that its aria-pressed attribute becomes "true" depends on the component's accessibility-tree contract, a behavioral contract rather than an implementation detail.
The most durable assertions check what the user can observe. After a form submission, verify that a confirmation message appears, rather than that a particular state variable was set. After an error, verify that the user can see an error message. The implementation may change as long as the visible result remains correct.
Independent setup and teardown
A test relying on state left by an earlier test may pass in sequence but fail alone or in another order. This failure is unintuitive because the test is not itself wrong. Its reliance on outside state is what makes it fragile.
Long-lived tests create the precise state they need at the start of every run and clean up afterward. In Playwright, that usually means fixture-based setup for a fresh user account or defined database state before each test, rather than shared state left by an earlier test in the file.
The initial cost is a little higher because fixtures must be written and maintained. Over time, each test becomes a self-contained unit whose result depends only on its own behavior. Parallelising or reordering the suite to cut runtime then causes no breakage.
What breaks tests that seemed permanent
We do not claim that a well-written test cannot fail. Major product changes, such as removing a feature or replacing a component with a completely different UX pattern, should break the test for that behavior. That means the test is doing its job.
A structurally sound test does not break when CSS classes are renamed, the DOM is reorganised without changing semantics, unrelated element text changes, performance work changes timing, or internal state management is refactored. These changes consume much of a suite's maintenance budget, while tests with the properties above absorb them quietly.
While building the repair system in TestSprighte, we found that tests needing the least intervention followed these patterns. Locator drift was concentrated in tests using CSS class selectors and DOM position queries. Tests based on accessible roles and explicit data attributes either survived untouched, or failed for a meaningful reason: the accessible role had really been removed, which is a genuine problem worth finding.