TechByteByByte

Part 39: Best Practices and Anti-Patterns

Consolidate practical patterns and avoid the habits that make automation brittle.

Best practices strengthen trustworthy evidence; anti-patterns are tempting habits that repeatedly create confusion.

clear intent + isolation + meaningful checks → trust

Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com

Every part of this series has included its own “common mistakes” thread, woven directly into the material where it belonged. This part pulls all of it together into one consolidated, reasoned reference — not as a list to memorize, but as a single, coherent worldview: every one of these recommendations exists because of a specific, real, understandable reason, and you now have the full context to genuinely understand every single one, rather than just accept it.


Locator Strategy

The practice:

prefer getByRole, getByLabel, and getByTestId over CSS classes, and prefer both over XPath, especially absolute XPath (Part 7). The reason:

locators built around what an element fundamentally is and means survive UI and styling changes; locators built around incidental implementation details (a shared class, an exact tree position) break the moment those unrelated details change, even though the element’s actual purpose never did.

Hard Waits

The practice:

avoid waitForTimeout as a default; rely on web-first assertions’ auto-retry instead (Part 9, Part 12). The reason:

a fixed wait is a guess — wasteful when the guess is too long, unreliable when it’s too short, and a direct, well-documented cause of flaky tests that pass on a fast machine and intermittently fail on a slower or busier one.

Page Object Model

The practice:

use POM to centralize locators and interactions for a page, but don’t over-engineer it for small projects, and don’t bury every assertion inside page object methods (Part 20). The reason:

POM solves a genuine duplication problem that scales with suite size — but that problem doesn’t exist yet in a five-test project, and burying assertions makes tests unreadable, obscuring exactly what’s actually being verified.

Fixtures

The practice:

use fixtures for genuinely reusable setup, choose scope (test vs. worker) deliberately based on whether state needs to be isolated per test (Part 15). The reason:

worker-scoped fixtures save real setup cost but share state across tests in that worker — using worker scope for something that genuinely needs per-test isolation silently undermines the very isolation the rest of Playwright’s architecture is built to guarantee.

Shared State

The practice:

generate unique, dynamic test data rather than reusing fixed values for anything your test creates, especially under parallel execution (Part 21, Part 27). The reason:

browser context isolation protects browser-level state automatically, but does nothing to protect shared external resources — a fixed email, a shared database row — from collisions between tests running concurrently or even just sequentially across multiple runs.

Credentials and Secrets

The practice:

never hardcode real, sensitive credentials directly in source code; use environment variables and .gitignored .env files, or a CI secrets manager (Part 5, Part 16, Part 21). The reason:

anything committed to Git remains in the repository’s history indefinitely, even after being “deleted” in a later commit — the only real protection is never committing it in the first place.

Test Data

The practice:

clean up data your tests create, ideally in afterEach rather than only at the end of a test’s own linear code (Part 21). The reason:

afterEach runs regardless of pass or fail, so cleanup still happens even when a test fails partway through — precisely the situation most likely to otherwise leave orphaned, contaminating data behind.

Assertions

The practice:

every test should contain genuine, meaningful assertions verifying an actual outcome, not just a sequence of actions (Part 9). The reason:

a test with no assertions can pass indefinitely regardless of whether the feature it’s meant to verify actually works — it isn’t testing anything, it’s just performing steps.

Mocking

The practice:

use real backend calls as your default for genuine end-to-end tests; reserve mocking for testing frontend behavior in conditions that are difficult or unsafe to reproduce with a real backend (Part 19). The reason:

an over-mocked suite can pass consistently while the real, actual integration between frontend and backend is silently broken, since nothing in the suite ever genuinely exercises that real connection.

Retries

The practice:

use a small number of retries to absorb genuinely transient environmental noise in CI; never use retries as a substitute for diagnosing and fixing genuine flakiness (Part 16, Part 28). The reason:

a retry doesn’t address whatever is actually causing inconsistency — it hides the symptom while the underlying, unstable root cause remains fully present and will keep recurring.

Flaky Tests

The practice:

quantify flake rate before attempting a fix, categorize the actual root cause, and fix that root cause specifically rather than reaching for a generic remedy (Part 28). The reason:

“flaky” describes several genuinely distinct problems that happen to look identical from the outside — treating them interchangeably wastes time and often doesn’t actually resolve anything.

Framework Architecture

The practice:

separate concerns into distinct folders (pages, fixtures, test data, API helpers) so each piece has exactly one clear reason to change (Part 31). The reason:

when responsibilities are cleanly separated, a single real-world change — like a UI redesign — only requires touching the specific files actually responsible for that concern, rather than hunting through and modifying scattered, intermixed code throughout the whole codebase.


The Underlying Meta-Principle

If there’s one single idea underneath every single item above, it’s this, worth holding onto as your genuine default posture toward this entire craft, beyond any individual rule: a good automation practice is rarely “correct because it’s the rule” — it’s correct because of the specific, real, understandable consequence it prevents. A locator strategy isn’t good because a style guide says so; it’s good because it survives real changes a bad one wouldn’t.

A wait strategy isn’t good because it’s “best practice” as an empty phrase; it’s good because it eliminates a genuine, specific category of race condition.

This series has deliberately tried to build every single recommendation from that kind of reasoning, all the way from Part 0’s very first sentence, specifically so that when you eventually encounter a genuinely new situation this series never explicitly covered, you have the actual underlying reasoning skill to work out the right answer yourself — which is the real, lasting goal, far more than any individual rule memorized in isolation.

Analogy: The Safety Harness vs. Sewing Thread Imagine securing heavy luggage on top of a car roof rack:

  • Best Practices (The Safety Harness): You wrap thick, high-tension nylon ratchet straps through the frame, lock the steel hooks, and check the tension. The car can hit potholes, swerve, and navigate mountain curves at 70 mph, and the luggage remains completely secure (web-first auto-retries, unique data isolation, getByRole locator stability).
  • Anti-Patterns (Sewing Thread): You tie the luggage using standard cotton sewing thread because “it’s fast, looks neat, and holds perfectly fine in the garage” (hardcoded timeouts, absolute XPaths, shared test users). The moment the car pulls onto the actual highway and hits a small bump (slight network delay, database collision, layout shift), the threads snap instantly and your cargo scatters across the road.

📊 Visual Flowchart: The Diagnostic Best Practices Hierarchy Checklist

Here is how you evaluate and prioritize framework improvements across different automation layers:

graph TD
    classDef pass stroke:#2ecc71,stroke-width:2px;
    classDef warn stroke:#f39c12,stroke-width:2px;
    classDef fail stroke:#e74c3c,stroke-width:2px;

Start["Inspect codebase quality status"] --> Layer1["1. Locator Layer"]
    Layer1 -->|Is using absolute XPath / class selectors?| LFail["Switch to: getByRole() / getByLabel()"]:::fail
    Layer1 -->|Is using semantic role locators?| Layer2["2. Waiting Layer"]:::pass

Layer2 -->|Is using waitForTimeout()?| WFail["Switch to: Web-First Asserties auto-retry"]:::fail
    Layer2 -->|Is using auto-waiting expect()?| Layer3["3. State & Data Layer"]:::pass

Layer3 -->|Is sharing a single test user account?| DFail["Switch to: Dynamic Faker.js user builders"]:::fail
    Layer3 -->|Is isolating data per test run?| Layer4["4. Security Layer"]:::pass

Layer4 -->|Are real credentials hardcoded?| SFail["Switch to: gitignore .env / CI Secrets"]:::fail
    Layer4 -->|Are secrets safely loaded?| Ready["Suite status: Robust & Production-Ready"]:::pass

How It Works in a Real Test Run

The practices in this part all protect one goal: a failed test should provide trustworthy evidence about one behavior. User-facing locators, condition-based waits, isolated data, focused assertions, careful mocking, and useful traces make that evidence easier to trust.

A practical review asks: can the test run alone, in parallel, and repeatedly; does it prove a user-important outcome; will its error identify the broken stage; and is the cost justified by the risk it covers?

Interview Questions

Q: If someone asks you “what’s your locator strategy,” what’s a genuinely strong answer, beyond just naming getByRole as a preference?

Ans: I’d explain the underlying principle first — prioritizing locators built around what an element fundamentally is and means (its role, its accessible name) over locators built around incidental implementation details like CSS classes or DOM position, because the former survives unrelated UI and styling changes while the latter tends to break alongside them. I’d name the actual priority order (role, label/text, test-id, then CSS/XPath as a last resort) and be able to justify each step of that ordering with a concrete example of the kind of change it does or doesn’t survive.

Q: Why is “add a retry” often the wrong first response to a flaky test, even though it frequently makes the test pass consistently afterward?

Ans: Because a retry doesn’t address whatever is actually causing the underlying inconsistency — it just gives the test additional attempts to happen to succeed despite that instability, which is still fully present and will keep recurring, potentially at a worsening rate as the suite grows and runs more frequently in parallel. Treating this as a genuine fix, rather than a temporary mask, means the real root cause is never actually investigated or resolved.

Q: What’s the single biggest risk of over-mocking an entire test suite, and why doesn’t a fully passing suite necessarily mean the application is actually working?

Ans: The biggest risk is that the suite can pass consistently and confidently while the real, genuine integration between frontend and backend is silently broken, since no test in an entirely mocked suite ever actually exercises that real connection. A fully passing suite in that scenario only proves the frontend behaves correctly given the assumed responses baked into the mocks — it says nothing about whether those assumptions actually match what the real backend genuinely returns.

Q: Someone asks you to justify why your team invests time in Page Object Model for a large suite. What’s the strongest, most concrete justification you can give?

Ans: I’d point to the concrete cost of duplication without it — a single UI change requiring updates across every test file that happened to duplicate the same locator or interaction logic — versus the cost with POM, where that same change requires updating exactly one centralized page object class, with every test using it automatically fixed. I’d frame this specifically as a cost that scales with suite size, which is exactly why it’s worth the investment for a large, growing suite even though it would be unnecessary overhead for a very small one.

Q: What is the single underlying principle connecting nearly every best practice covered across this entire series?

Ans: That a good automation practice is grounded in a specific, real, understandable consequence it prevents, not in being an arbitrary rule to follow. Every recommendation in this series — from locator strategy to waiting behavior to test data management — was built from reasoning about the actual problem it solves, specifically so that the underlying reasoning transfers to genuinely new situations, rather than only working for the exact scenarios explicitly covered.


Exercises — Part 39

Understand: Pick any three best practices from this part and, without looking at the “reason” given, write your own explanation, in your own words, of why each one matters — then compare your reasoning against what’s written here.

Simple Practice: Review a test file you wrote early in this series (Parts 6–9) with fresh eyes, and identify at least two places where your current understanding, built across the rest of the series, would lead you to write it differently today.

Real-World Scenario: Imagine you’re reviewing a teammate’s pull request containing: a test.only left in, a waitForTimeout(3000), a hardcoded email in a registration test, and a locator using an absolute XPath. Write out, as an actual code review comment, a clear, respectful explanation of each issue and your suggested fix, drawing directly on this part’s reasoning.

Challenge: Write your own, personal “top five” list of the best practices from across this entire series that you believe matter most, ranked in order of importance, with a one-paragraph justification for your ranking — there’s no single correct answer here; the value is in articulating and defending your own genuine reasoning.


Next: Part 40 — Real-World Capstone

— building a complete automation framework from scratch, applying every concept from this entire series to one cohesive project.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed