AssertAssert
Guide

End-to-End Testing Best Practices for Engineering Teams

Most teams that struggle with E2E testing are not failing because of bad tooling. They are failing because of a small number of authoring and workflow habits that compound into a maintenance crisis over time. The good news is that the same habits that prevent that crisis are straightforward to adopt from the start.

Test user journeys, not implementation details

The most durable E2E tests are written from the perspective of a user completing a task, not from the perspective of a developer who knows how the component tree is structured. 'Fill email, fill password, click Sign in, expect Dashboard' is a user journey. 'Find the input with class auth-form__email-input and set its value' is an implementation detail.

Implementation details change constantly. User journeys change far less often. Writing tests at the user journey level means they survive most refactors, component library upgrades, and styling changes without modification.

Keep selectors stable and intentional

Every selector in a test is a dependency on the current state of the DOM. The more selectors your tests use, and the more fragile those selectors are, the more maintenance work your team will do as the product evolves.

Prefer role and label-based selectors — they are tied to how the element behaves and is described, not how it is rendered. Add `data-testid` attributes for elements that lack accessible names. Treat CSS class selectors, positional selectors, and XPath as code smells — each one is a maintenance liability.

  • Use `getByRole`, `getByLabel`, and `getByText` as the default — they survive most refactors
  • Add `data-testid` attributes to key interactive elements that lack natural labels
  • Never use CSS class names as selectors — they change with styling refactors
  • Never use `nth-child` or positional selectors — they break when layout changes
  • Review selector choice in code review the same way you review logic

Write focused tests with clear scope

Each test should cover exactly one user journey, with a clear setup, a defined sequence of actions, and a meaningful assertion. Tests that cover multiple unrelated scenarios are harder to name, harder to debug when they fail, and harder to update when the product changes.

A good rule of thumb: if you cannot describe what a test is protecting in a single sentence, it is trying to do too much. Split it.

Treat test files like product code

Test files that are not reviewed, not versioned carefully, and not updated alongside the code they protect will drift and decay. The habits that keep production code healthy — code review, clear naming, meaningful commit messages — apply equally to test code.

When a UI change requires a test update, that update should go in the same pull request as the UI change. This creates a culture where maintenance is routine rather than deferred, and where the test suite always reflects the current product.

Run tests in CI on every pull request

Tests that only run manually provide a fraction of their potential value. Automated CI runs on every pull request catch regressions before they merge, make failures visible to the whole team, and gradually build confidence that the suite is worth maintaining.

Start with critical flows as required checks and expand from there. A small, reliable CI suite is more valuable than a large, flaky one that engineers learn to ignore.

Keep the human-readable artifact in the repo

Whether you use raw Playwright or a higher-level tool like Assert, the definition of what each test is protecting should be readable and versioned in the repository. That definition is what enables code review, maintenance, and confident updates when the product changes.

Assert's Markdown scenario model is built around this principle. The scenario file — plain English, in the repo — is the primary artifact. The Playwright execution is generated underneath it.

FAQ

How many E2E tests is the right number?

There is no universal answer, but a useful frame is: cover every flow where silent breakage would cause a support incident or directly impact revenue. For most products, that is between five and twenty scenarios. Broad coverage of edge cases is less valuable than reliable coverage of critical paths.

What is the biggest mistake teams make with E2E testing?

Writing tests at too low a level — targeting DOM structure rather than user intent. This creates a suite that breaks constantly as the product evolves, trains engineers to ignore failures, and eventually gets abandoned. Starting at the user journey level and keeping tests readable is the single most impactful habit.

How do you handle test data in E2E tests?

Use dedicated test accounts and test data that you control. Avoid coupling tests to production data that can change between runs. For tests that create data (like creating a ticket or placing an order), clean up that data after the test or use an isolated test environment where cleanup is automatic.

Put the workflow in your repo, not in a chat transcript

Assert is strongest when scenarios become durable project assets: readable Markdown in the repo, generated execution underneath, and result inspection in the dashboard.