← All Posts

E2E Testing Anti-Patterns: 7 Mistakes That Kill Your Test Suite

3/5/2025

After working on dozens of test suites across SaaS, e-commerce, and fintech, I keep finding the same mistakes. These aren't beginner errors. Senior engineers make them too, because they seem reasonable until the suite hits 200+ tests.

1. Testing through the UI when you should use the API

If your test needs to create 3 users, 2 projects, and 5 tasks before testing a dashboard filter, don't do that through the UI. It's slow, fragile, and the test isn't even about user creation.

Fix: Use API calls in your test setup to create preconditions. Reserve UI interactions for the actual behavior you're testing. A test that takes 45 seconds through the UI often takes 5 seconds with API setup.

2. Sleeping instead of waiting

await page.waitForTimeout(3000) is never the answer. It's too slow when things are fast, and too short when things are slow. It's the #1 source of flaky tests.

Fix: Wait for specific conditions: network responses (waitForResponse), element visibility (toBeVisible), or URL changes (waitForURL). If you can't find a good wait condition, the app probably needs a loading indicator.

3. Shared test data across tests

Tests that share a database user, a project, or any mutable data will eventually conflict. Test A modifies data that Test B depends on. When they run in parallel (or in a different order), one fails.

Fix: Each test creates its own data and cleans up after itself. Use unique identifiers (timestamps, UUIDs) in test data to avoid conflicts.

4. Testing everything end-to-end

Not every test needs a browser. If you're testing that a discount calculation returns the right number, that's a unit test. If you're testing that an API rejects invalid input, that's an integration test. E2E tests should cover user journeys, not business logic.

Fix: Push tests down the pyramid. E2E tests for critical paths only. Everything else should be unit or integration tests.

5. Brittle selectors

Tests that select elements by CSS class (.btn-primary), DOM structure (div > span:nth-child(3)), or generated class names (.css-1a2b3c) break on every design change.

Fix: Use data-testid attributes for test-specific selectors. Or use Playwright's role-based selectors: getByRole('button', { name: 'Submit' }). These survive UI refactors.

6. No test isolation in CI

Tests pass locally but fail in CI. Or tests pass individually but fail when run together. This is almost always a shared state problem: cookies persisting between tests, a global variable being mutated, or tests hitting the same database rows.

Fix: Use fresh browser contexts per test (Playwright does this by default). Use separate test databases or transaction rollbacks. Run your full suite locally in parallel mode before assuming CI is the problem.

7. No ownership or maintenance process

Tests without owners rot. Nobody updates them when features change. Failures get ignored. The suite becomes a 30-minute CI tax that nobody trusts.

Fix: Assign test ownership to feature teams. Add test maintenance to sprint planning. Quarantine flaky tests immediately instead of ignoring them. Track test health metrics the same way you track uptime.

The pattern

All 7 anti-patterns share a root cause: treating tests as a second-class citizen. Teams that treat test architecture with the same rigor as production architecture don't hit these problems. Tests are code. They deserve code reviews, refactoring, and architectural thinking.

Recognize these patterns in your suite? Reach out. We audit test suites and fix exactly these issues for engineering teams.

Need Help Implementing This?

We help engineering teams set up test automation, CI/CD, and quality infrastructure.