All articles
QA AutomationBestPracticesPageObject

Test Automation Best Practices, Before You Learn Any Tool

Most people start with the tool and skip the fundamentals, and they end up with fragile tests that are hard to maintain. These are the principles that make the difference, and they work with any tool.

9 min read

Most people who get into test automation open a Selenium or Playwright course on day one and learn the tool well. Six months later they find themselves with a hundred flaky tests, and every UI change breaks half of them.

The problem isn't the tool. Tools change every few years; principles don't. Someone who has mastered them moves from Selenium to Playwright in a week. Someone who hasn't repeats the same mistakes with a newer tool.

1. One test, one function

Every test should check one specific thing. If you feel your test is checking more than one function at once, that's a sign you need to split it.

Why? Because when it fails, you want to know what broke from the test name alone. A test that checks sign-up, login and checkout together tells you "something is wrong somewhere" when it fails, and that isn't information.

Tip
The test name is a great check: if you can't name it in one sentence without "and", it's testing more than one thing. "The user can add a product to the cart" is a test. "The user signs up, adds a product and pays" is three.

2. Every test stands on its own

No test depends on the one before it. Not on its data, not on state it left behind, and not on the run order.

The easiest way to make sure: run the test on its own. If it passes alone and fails as part of the suite, it depends on something else and you don't know it.

3. Every UI step is a chance for flakiness

This is the most important point in the whole article. Every click and every field you fill is a new chance to fail. A test with 22 UI steps won't be stable, no matter how well you write it.

The fix is to use the API for the setup steps, and keep the UI for the part you actually want to test:

22 UI steps 3 UI steps Setup via the API

A practical example: instead of registering a new user through the UI, adding products to the cart through the UI, placing the order through the UI, and then opening the order to download the invoice, register the user via the API and create the order via the API, then open the UI and download the invoice. From 22 steps down to three.

And the sign-up steps in the UI? They get their own test, one that checks sign-up specifically.

4. Locator strategy

This is the single biggest source of maintenance cost, and the one people notice last.

A locator based on the shape of the page breaks with the first design change:

  • div > div:nth-child(3) > span: breaks if the designer adds a single element
  • A long XPath from the top of the page: same story
  • Auto-generated CSS classes like css-1x9f8k: they change with every build

A locator based on intent holds up:

  • data-testid: an attribute that lives in the code for one purpose only: testing
  • The role and name the user sees: a button called "Log in"
  • The visible text, or the label attached to the field

The rule, simply: a locator should describe what the element is, not where it sits on the page.

Tip
If the code doesn't have data-testid, add it yourself. You know HTML, and you have access to the same repository. It's one line in a frontend file, and you open a pull request like anyone else on the team. Don't wait for someone to do it for you, and don't park it in the backlog. You know better than anyone where you need the locator and why, so you're the right person to add it. A tester who changes the code has become part of the team, not an outsider making requests.

5. Waiting strategy

No fixed sleep. It's slow and unreliable at the same time: five seconds is too long when the page loads in one second, and too short when the server crawls. The result is tests that are slow and flaky at once.

The alternative is to wait for the condition, not for time: for the element to appear and become clickable, for the request to finish, for the text to change.

The good news: most modern tools do this for you. Playwright and Cypress have built-in auto-waiting: before any interaction they make sure the element exists, is visible and is ready, so you rarely need to write a manual wait at all. In Selenium you use WebDriverWait with an explicit condition.

So in most cases, the solution to this problem is to not fight the tool: let it wait its own way, and don't jam a sleep into the middle.

6. Page Object Model

Instead of one long test file packed with code, spread the code across files: one file per page. The login page, sign-up, the cart. Each file holds its own locators and functions.

The result: tests become easier to read, and maintenance happens in one place.

7. Test the behaviour, not the implementation

In the page object, define a function called login and put the steps inside it: fill in the email, fill in the password, click the button. In the test file, just call login().

That way, when you read the test file you read what you're testing, not how you're testing it. And when something changes in login (say they add a checkbox), you fix it in one place, not in every test.

8. Assertions live in the test file

Keep the assertions in the test file, not inside the page object. When you read the test you should see exactly what it's verifying.

There's an important side benefit here: if you find lots of tests making the same assertion, that's a warning that you're repeating yourself and that some tests are redundant. Which is also why a shared assertion function in the page object isn't a useful idea in the first place: it shouldn't be used more than once.

9. Test data lives with the test

Every test has its own data. Keep it visible in the test file so you can understand the scenario without opening other files.

Put the data management itself in a separate file: a Factory that generates data at runtime: an order with a discount, an order without one, a new user with a unique email every time. That way you build the scenario you want in one line, without relying on pre-existing data in the environment.

10. Build a mock server

Use whatever tool suits you (WireMock, Express, NestJS or something else) and hook your tests up to it. That way you work at the integration level: faster, more stable, and full control over the responses.

The rule: 90% of your coverage at the integration level, positive and negative cases together. And E2E only for the most important user journeys.

Warning
Never do negative cases at the E2E level. A negative case needs specific responses from the services, and that's exactly what a mock gives you. At the E2E level it'll be slow and flaky, and it'll test less.

11. Seed your data, and clean up after yourself

You need a way to seed the data you need before the test, and clean it up afterwards.

Otherwise, within a few months the staging database will fill up with thousands of fake orders and users, until the environment itself becomes slow and unreliable. Always clean up when you're done.

12. Make debugging easy

When a test fails, you should know why within seconds, not within half an hour.

  • A clear report with screenshots, video and network data, and the ability to go back in time step by step. Modern tools like Playwright and Cypress give you this out of the box.
  • Only on failure. If you record every run, the server will fill up with screenshots nobody will ever open.
  • And clean up the reports regularly with a scheduled job.

In the end

Notice that not one of the principles above is tied to a particular tool. They all work with Selenium, with Playwright, and with whatever comes after them.

That's the difference between someone who "knows Playwright" and someone who knows test automation. The first one stalls when the tool changes; the second one takes everything with them.

So if you're just starting out: learn the tool, of course, but learn these principles while you're at it. And the tool will become a detail.

Want to go deeper?

This article gives you the idea. The courses take you from zero to actually doing it.

See the courses