When to start automating
Automation doesn’t replace a tester’s thinking, it repeats what the tester came up with first. That’s why it is worth starting with the basics of manual testing: test techniques, test cases and defect reporting. A good test case is easy to turn into an automated test, and a bad one automated is simply a bad test that runs faster.
This is the last part of the “Testing from scratch” series.
Choosing a tool
| tool | language | strengths | watch out for |
|---|---|---|---|
| Playwright | TypeScript, JavaScript, Python, Java, C# | auto-waiting, quick start, test recording, multiple browsers | younger than Selenium |
| Selenium | Java, Python, C#, JavaScript and others | the standard for years, many job ads and learning materials | you handle waiting yourself |
| Cypress | JavaScript, TypeScript | convenient debugging in the browser | limitations with multiple tabs and domains |
To start, I recommend Playwright with TypeScript: it is the fastest way to write a stable test. If you are aiming at a specific company, check the job ads to see what it uses. The principles you learn in one tool carry over to any other.
Your first Playwright test
You need Node.js. In an empty folder run:
npm init playwright@latest
The installer asks about the language and creates a sample project. A test looks like this:
import { test, expect } from "@playwright/test";
test("Get started link leads to installation", async ({ page }) => {
await page.goto("https://playwright.dev/");
await page.getByRole("link", { name: "Get started" }).click();
await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
});
Run it with npx playwright test and open the report with npx playwright show-report. npx playwright codegen is useful too: it records your clicks as code. Treat the recorded code as a draft to improve, not a finished test.
Locators: how to point at an element
A locator tells the test which element an action applies to. Locators matter more than anything else in deciding whether a test survives changes in the application.
From best to worst:
- Role and name, i.e. what the user and a screen reader see:
getByRole("button", { name: "Save" }). - Field label:
getByLabel("Email"). - Test attribute:
getByTestId("checkout-submit"), when developers have addeddata-testid. - Text:
getByText("Order confirmed"), good for checking messages. - CSS and XPath: only when nothing else works. A locator like
div > div:nth-child(3) > spanbreaks with every layout change.
If the application lacks good locators, ask the developers for data-testid. It takes them a few minutes and saves hours of fixing tests.
Waiting for elements
Web applications load data in the background, so the element you are looking for may appear after a moment. A test that doesn’t wait fails, even though the application works. This is the most common cause of flaky tests.
The worst solution: a fixed pause. sleep(5000) makes the test wait 5 seconds every time. When the app responds in half a second, you waste time. When it responds in 6 seconds, the test fails anyway.
In Playwright waiting is built in. Before clicking, Playwright checks by itself that the element is visible, stable and enabled, and assertions such as toBeVisible() retry the check until the timeout. In simple tests you don’t need to write any waiting at all.
In Selenium you handle waiting yourself. There are three kinds:
- implicit wait: a global time during which Selenium retries finding every element;
- explicit wait: waiting for a specific condition of a specific element;
- fluent wait: an explicit wait with a configurable polling interval and exceptions to ignore.
In practice, explicit wait works best:
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit"))).click();
Don’t mix implicit and explicit waits in one project. The Selenium documentation warns that wait times then become unpredictable.
Principles of good automated tests
- Independent tests. Each one prepares its own data and doesn’t assume another test ran first.
- One test, one behaviour. It’s easier to find the cause when a test fails.
- Data through the API, checks through the UI. You create a test account faster with a request to the API than by clicking through the sign-up form.
- Readable names. The test name in the report should tell you what is broken without opening the code.
- The most important paths first. Login, purchase, payment. Don’t start with the “About us” page.
As the number of tests grows, repeated code appears. That is when the Page Object pattern helps: classes with locators and actions for each page. For the first few tests it is overkill.
What next
Tests you only run by hand age quickly. The next step is running them automatically on every code change, which I describe in the guide to regression in CI. I cover the structure of a larger project and saving the login session in the guide to regression automation in Playwright.
Questions about waiting and locators come up regularly in interviews, including for manual roles. Test yourself on the automation section of the interview questions.
The “Testing from scratch” series
- What does a software tester do, and how to get started
- Testing theory in a nutshell
- How to write a test case
- How to report defects
- API testing basics
- First steps in test automation (this guide)