Test Automation Best Practices
The practices that keep an automated test suite an asset instead of a liability — from what to automate to how to keep it fast, trusted, and small enough to maintain.
Most advice about test automation is about writing tests. The harder part — the part that decides whether the suite is worth having in two years — is keeping it valuable as the product changes underneath it. These are the practices that do that, roughly in the order they matter.
Automate the right things
- Start with high-frequency, high-value, stable checks — the pre-release regression sweep and the critical user journeys (sign-up, login, checkout, the core loop). See what to automate first.
- Don't automate flows still in active redesign. You'll spend more time reconciling the test with weekly UI changes than the test will ever save you. Wait for the design to settle.
- Every escaped bug becomes a test before the fix merges. The reproduction is already known and the bug already proved it matters — this is the cheapest high-value coverage you'll write.
- Skip vanity coverage. A test that only fails when someone deliberately breaks it — asserting a constant, testing a getter — lifts the coverage number and catches nothing.
Test at the right level
- Write the test at the lowest layer that can actually catch the bug. A wrong calculation is a unit test. A serialisation bug between services is an integration test. A button that isn't wired to its handler needs something that drives the real UI. See choosing your test distribution.
- Don't over-mock. Push coverage too far down and you're testing that your code called certain functions, not that it produced the right result — see mock vs stub vs fake. Prefer a lightweight fake over a wall of mocks.
- Keep the end-to-end layer thin. It's the most expensive to write, the slowest to run, and the most brittle. Reserve scripted E2E tests for the exact assertions that must hold on every release, and let broader coverage come from another layer.
Make tests deterministic
Flakiness is the single biggest threat to a suite's value, because it destroys trust. The main causes and their fixes:
- Wait on state, never on time. Wait for an element to be interactable or a request to complete — never
sleep(2000). Fixed waits are both slow and unreliable. This is the top cause of flaky E2E tests — see why E2E tests get flaky. - Isolate test data. Each test sets up its own starting state and tears it down. If tests can't run in a random order, they're coupled, and coupling produces order-dependent failures.
- Select by role or intentional test IDs, never by a deep CSS path or an auto-generated class. A styling change should never break a functional test.
- Stub flaky third parties at the boundary. Where you need the real integration, isolate those tests and allow bounded retries.
- Kill flaky tests fast. Quarantine them out of the blocking path immediately, then fix or delete. A flaky test left in CI trains the whole team to ignore red.
Keep the suite maintainable
- Prune quarterly. Delete tests that only ever fail for maintenance reasons, or that no longer map to a real risk because the feature changed. A shrinking suite that's all signal beats a growing one that's half noise.
- Watch the run time. If the suite is slow enough that developers skip it locally and just push, it has stopped protecting the inner loop. Parallelise, or move slow checks to a later stage.
- Review coverage by journey, not by percentage. "Is checkout covered end to end?" is a useful question. "We're at 82%" is not — see test coverage.
- Give the suite an owner. An end-to-end suite with no one responsible for its health degrades into flakiness within a couple of quarters.
Run it automatically
A test that doesn't run without someone asking doesn't protect you. Wire the layers to the right triggers:
- Static analysis + unit tests — on every commit and pull request. Fast, precise, cheap.
- Full integration suite + a thin scripted E2E set — on merge to the main branch.
- Broad end-to-end and autonomous runs — triggered after each deploy to staging.
See testing in CI for the full breakdown.
Reduce what you have to maintain
Every practice above gets easier the smaller the maintained suite is. The most effective single move is structural: layer your coverage so the high-maintenance scripted end-to-end part stays minimal and autonomous coverage carries the breadth. There is nothing to maintain in a layer that re-explores the app on every run — no selectors, no waits, no fixtures drifting out of sync. That keeps the part your team actually maintains small enough to keep healthy.
Bottom line
Automate the stable and high-value first, test at the lowest useful layer, make every test deterministic, prune aggressively, give the suite an owner, run everything in CI, and — above all — keep the maintained portion as small as you can. The test automation guide puts these into a coherent strategy.