Let’s not sugarcoat it—automated testing has a reputation. Slow. Flaky. Frustrating. Most developers know they should write tests, but few enjoy doing it. And for beginners, just setting things up feels like solving a Rubik’s cube blindfolded.
But here’s the reality: modern web apps aren’t simple anymore. They’re fast-moving, JavaScript-heavy beasts—built with frameworks like React, Vue, or Angular, packed with real-time data, modals inside modals, infinite scrolls, and micro-interactions. If you’re not testing well, you’re shipping risk.


That’s why Playwright matters.
Playwright is the framework that finally gets out of your way. It’s built to handle modern web complexity—out of the box. Multi-browser support? Built in. Auto-waits for elements? Built in. Powerful selectors, cross-platform testing, headless mode, parallel execution? Yep, all in there. You don’t need plugins or duct tape to make it usable. It just works—and it works well.
And if you’re thinking, “Okay, sounds promising… but where the hell do I start?”—you’re not alone.
This guide is built for people like you. New to automation. Curious but cautious. Maybe you've dabbled in Selenium, or maybe you’ve avoided testing altogether. Either way, we're cutting the jargon and showing you how to get productive fast—with real explanations, actual code, and no filler.
Want to go deeper? Uncodemy’s Playwright Automation Testing course is built exactly for this moment. It walks you through the real stuff—how to install and set up Playwright the right way, how to structure tests so they scale, how to avoid common flakiness, and how to write tests that don’t break every time the UI shifts by 5 pixels. No fluff, just hands-on, job-ready skills.
Whether you're aiming to become a QA automation engineer, a full-stack dev who owns their testing stack, or someone trying to level up before your next job switch—Uncodemy’s course gives you the structure and depth you need.
Now, let’s get our hands dirty.
Setting up testing tools can feel like pulling teeth. You follow some official docs or a YouTube tutorial from 2021, and suddenly you’re knee-deep in version mismatches, cryptic errors, and npm yelling at you about peer dependencies. Frustrating.
But getting started with Playwright doesn’t have to be a rage-inducing experience. In fact, it's probably one of the smoothest setups in the web testing world right now—if you approach it with the right steps and skip the noise.
No complicated checklist here. Just make sure you have:
If you don’t have it, grab the latest LTS version from nodejs.org. You don’t need to install nvm or mess with environments unless you know you need that.
You can start fresh with a new folder or use an existing frontend/backend project. Playwright works standalone or alongside your app.
We recommend VS Code because the Playwright extension for it is a godsend. But honestly, use whatever editor you feel comfortable in.
If you're the kind of learner who likes someone walking you through all of this line by line—no judgment— Uncodemy's Playwright course does exactly that. It's made for folks who want to learn by doing, not by memorizing.
Open your terminal in your project directory and type:
npm init playwright@latest
This is Playwright’s setup wizard, and it’s pretty great. It asks you a few questions, like:
It’ll install the necessary packages, download the browsers, and scaffold a basic project structure. You don’t need to hunt for dependencies, install three other packages, or configure your test runner separately. It’s all baked in.
Once it finishes, you’ll have something like this:
tests/
│
├── example.spec.ts // Sample test to get started
playwright.config.ts // Your global config file
package.json // Scripts and dependencies
Go ahead and run your first test:
npx playwright test
It’ll launch in headless mode, run the example, and spit out results. If you see green, you’re golden.
This part trips people up, so let’s clarify:
That’s it. No layers of abstraction or legacy junk to worry about.
If you’re using VS Code, go install the Playwright Test for VS Code extension. It gives you:
It’s not essential, but it’s one of those tools that makes testing feel less like work and more like you’re in control.
The takeaway here is simple: Playwright is one of the easiest modern tools to set up, but only if you understand what’s actually happening during installation.
The key is not to treat this like a “set it and forget it” step. Understand the structure. Know what’s being installed. Keep your project organized from day one.
And if you want to cut your learning curve in half, take the route that’s made for real people: Uncodemy’s course on Playwright. It’s hands-on, no-BS, and packed with practical examples.
So you’ve installed Playwright. You ran the example test. It blinked open a browser, did its thing, and now you’re staring at an empty test file thinking, “Alright... what now?”
This is where most people get stuck. Not because Playwright is hard, but because every guide either talks down to you or gives you abstract code that doesn’t feel even remotely like your actual project.
Let’s fix that. No buzzwords, no fake examples—just the kind of stuff you’ll actually write.
If you’re building anything remotely useful, you’ve got a login page. So let’s start there.
Here’s the simplest working test:
import { test, expect } from '@playwright/test';
test('can visit login page', async ({ page }) => {
await page.goto('https://your-app.com/login');
await expect(page).toHaveURL(/login/);
});
That’s it. If you’re new, don’t worry about being clever. Just focus on small, working pieces.
Also: group your tests by feature. Don’t dump everything into one tests/ folder like a wild raccoon.
Now let’s make the test do something.
test('logs in with valid credentials', async ({ page }) => {
await page.goto('https://your-app.com/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('mypassword');
await page.getByRole('button', { name: 'Log In' }).click();
await expect(page).toHaveURL('https://your-app.com/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
});
That’s your full login path. You go to the page, fill in the form, click the button, and check what happens next.
No waitForTimeout, no guessing. If something breaks, it’ll tell you.
You’re not going to want to repeat that login block in every test. That’s a recipe for chaos. So abstract it:
// helpers/login.ts
export async function login(page, email, password) {
await page.goto('https://your-app.com/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Log In' }).click();
}
Use it like this:
import { login } from '../helpers/login';
test('dashboard shows correct greeting', async ({ page }) => {
await login(page, 'user@example.com', 'mypassword');
await expect(page.getByText('Welcome')).toBeVisible();
});
Now your tests read like actual sentences. That’s what you want.
Most apps don’t have just one user. There’s admins, editors, guests, you name it. You can loop through them like this:
const users = [
{ email: 'admin@example.com', password: 'admin123', role: 'Admin' },
{ email: 'user@example.com', password: 'user123', role: 'User' },
];
for (const { email, password, role } of users) {
test(`${role} can log in`, async ({ page }) => {
await login(page, email, password);
await expect(page.getByText(`Welcome, ${role}`)).toBeVisible();
});
}
This saves you from writing two near-identical tests. And you’re still testing real-world scenarios.
It’s not enough to test happy paths. You need to make sure errors show up when they’re supposed to.
test('shows error on bad login', async ({ page }) => {
await page.goto('https://your-app.com/login');
await page.getByLabel('Email').fill('fake@example.com');
await page.getByLabel('Password').fill('wrongpassword');
await page.getByRole('button', { name: 'Log In' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
});
This is just as important as the success case. Maybe more.
Once you’ve got a lot of tests, running login every time is going to slow things down. You can store the session state and skip the login step altogether.
First, record a session:
npx playwright codegen https://your-app.com/login --save-storage=state.json
Then use it in your config:
test.use({ storageState: 'state.json' });
Boom. Your test starts with the user already logged in. It’s not cheating—it’s efficient.
All without having to install anything weird or fight the syntax.
That’s how you write real tests in Playwright.
You don’t need 1,000 tests to have a good suite. You need:
That’s the goal.
And if this still feels overwhelming, Uncodemy’s Automation Testing with Playwright course walks you through all of this—CI setup, test strategy, mocking, flaky test fixes, everything. It’s not just another syntax tutorial. It's how real teams actually test modern apps.
Failing tests are annoying. Especially when they used to pass, nothing’s changed (or so you think), and now your CI is screaming in red. It’s like trying to find a typo in a 1,000-line essay—except the typo sometimes disappears when you’re not looking.
That’s the thing about automation. Even with the best tools, flaky or failing tests happen. But if you know how to troubleshoot properly, Playwright makes it way easier than most.
So let’s break it down—here’s how to debug Playwright tests like a calm, capable human being, not someone ready to throw their laptop out the window.
1. Use --debug to Watch the Test Like a Movie
Your first move should never be “hmm maybe if I rerun it 3 more times it’ll fix itself.” Instead, fire up the debug mode:
npx playwright test login.spec.ts --debug
This opens a real browser where you can literally see what the test is doing. It pauses between steps so you can check if the right button is being clicked, if the page loaded properly, or if something’s just plain missing.
Bonus tip: slap test.only() on the one test you’re trying to fix. No need to wait on the whole suite.
2. Pause the Test Mid-Flight
Sometimes you don’t want to watch the whole test. You want to stop right before the weird stuff starts.
Here’s your move:
await page.goto('https://yourapp.com/dashboard');
await page.pause(); // Stops here, lets you poke around
This brings up the Playwright Inspector. It’s like DevTools, but inside your test environment. Click around, inspect elements, even run JavaScript in the console.
It's a game changer, especially when you’re dealing with dynamic UIs or hidden elements.
3. Tracing = Instant Replay for Tests
If a test passes locally but fails in CI, don’t panic. That’s what Playwright’s tracing feature is for. It records everything your test does so you can replay it later like a full match highlight reel.
Enable it like this in your config:
use: {
trace: 'on-first-retry',
}
When a test bombs, run this:
npx playwright show-trace trace.zip
You’ll get a visual timeline: DOM changes, network requests, console logs, the works. It's like being able to rewind real life.
4. Bad Selectors = Most Common Reason Tests Fail
If a test says it clicked a button but nothing happened, chances are the selector was off.
Instead of writing brittle CSS like this:
await page.click('button.submit');
Use the Playwright-recommended approach:
await page.getByRole('button', { name: 'Submit' }).click();
Or better yet, use data-testid attributes in your codebase:
<button data-testid="save-settings">Save</button>
await page.getByTestId('save-settings').click();
It’s cleaner, more stable, and less likely to break when someone moves things around in the UI.
5. Don’t Ignore App Logs
Most apps log useful stuff to the browser console—errors, warnings, failed API calls. Playwright lets you hook into that:
page.on('console', msg => {
console.log(`APP LOG: [${msg.type()}] ${msg.text()}`);
});
You’ll catch those hidden JavaScript errors or failed fetches that don’t show up in the terminal but totally kill your tests.
6. Screenshots & Videos: Not Fancy, Just Useful
You don’t need to record every test, but when something fails? You want that video.
In your Playwright config:
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
Now, when a test crashes, you can see exactly what the user would’ve seen. Screenshots freeze the moment; video tells the full story. Don’t skip this.
7. Mock Stuff That Breaks or Slows You Down
Some things you can’t control—slow APIs, random data, or third-party services that occasionally flake. Mock them.
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test Product' }]),
});
});
That way, your test doesn’t rely on a real network call—or someone else’s server behaving.
8. Simplify Until It Breaks
If you’re stuck and nothing makes sense, start removing lines. Reduce the test to something minimal:
await page.goto('https://yourapp.com');
await expect(page).toHaveTitle('Dashboard');
Now slowly build it back up. One step at a time. Eventually, you’ll spot what’s causing the crash.
Yes, it’s tedious. But it works. It always works.
9. Retry… but With Caution
If you’ve got one flaky test and you’re really sure it’s an edge case, Playwright lets you retry it:
test('flaky test', async ({ page }) => {
// test logic
}).retry(2);
Just don’t overuse this. If you're retrying everything all the time, you’re just hiding problems, not solving them.
10. You’re Not a Failure Because Your Test Failed
Seriously. Everyone hits flaky tests. Everyone gets stuck. You’re not dumb, and your code probably isn’t garbage.
Take a break, zoom out, and go at it again.
And if you want structured help? Real guidance from real people? Uncodemy’s Playwright course walks you through debugging, selectors, mocking, and more—with examples you can actually understand. No dry theory. Just the stuff you wish you knew sooner.
Alright, time for some real talk. If you’ve ever written tests that depended on live APIs, flaky databases, or other people’s services, you’ve probably wanted to punch your screen. Been there.
Here’s the thing: your tests shouldn’t care whether the backend is acting up or your Wi-Fi’s having a mood. That’s where mocking and stubbing step in—basically faking stuff so your tests don’t break because someone else’s code did.
Let’s break this down in plain terms.
Because you don’t want to test everything every time. You want to test your code. If your frontend breaks, it should be because of your logic, not because the “/orders” endpoint suddenly decided to return a 502.
Mocking helps isolate what you’re testing. It makes your test suite predictable, fast, and less of a dumpster fire.
Playwright lets you hijack any network request and serve your own made-up response.
Here’s a real-world example: you’re testing a page that fetches user data.
await page.route('**/api/user', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: 'Fake User',
role: 'intern'
})
});
});
Boom. That request is now yours to control. It never hits the real backend.
No Backend? No Problem
Let’s say the backend team hasn’t finished building the feature yet. But your PM still wants a demo by Friday. Mock it.
This is super common in fast-moving teams. Mock the API, build the frontend, and plug in the real thing later. No blockers. No excuses.
Don’t want to mock the whole endpoint? Just intercept and mess with one property.
await page.route('**/api/products', async (route, request) => {
const original = await request.fetch();
const json = await original.json();
json[0].price = 0; // Make it free for testing
route.fulfill({
response: original,
body: JSON.stringify(json)
});
});
Now your test sees a modified version without blowing up the whole response.
Stubbing in Your Tests: Not Just a Backend Thing
Sometimes you’re not mocking network calls—you’re stubbing browser APIs.
Let’s say your app uses navigator.geolocation. Playwright lets you fake that too.
await context.grantPermissions(['geolocation']);
await page.setGeolocation({ latitude: 12.9716, longitude: 77.5946 }); // Bangalore, baby
Now your app thinks the user’s in Bangalore. Try that with plain Selenium.
Don’t get carried away. If you mock everything, your tests might all pass, but your app still breaks in production. Not a good look.
The rule of thumb: mock only when the real thing is flaky, unfinished, or outside your control.
For your own logic, use real data flows as much as you can. If you’re testing a login form, hit the real endpoint. If you’re testing weather in Antarctica? Mock away.
Real API calls are slow. Like, really slow in CI pipelines. Mocking makes tests fly. You can go from a 10-minute test suite to 2 minutes just by cutting out network junk.
If all this sounds like magic but you’re still not sure how to apply it in a real app, check out Uncodemy’s Playwright course. They walk through mocking with actual codebases, show how to simulate edge cases, and help you build a test setup that doesn’t collapse under pressure. Totally beginner-friendly.
You’ve made it this far. Your tests are solid. They run fast. You’ve mocked flaky APIs, intercepted third-party nonsense, and even faked GPS like a pro. But if your test data is half-baked, confusing, or just copy-pasted from some random JSON file—you’re still building on sand.
So let’s fix that.
I’m not saying you need real names and emails from your production database (please don’t), but your mock data should still feel legit. If it’s too barebones, your tests won’t catch real-world issues. If it’s too messy, good luck understanding failures later.
Bad :
const user = { name: 'test', email: 'x@y.z' };
Better :
const user = {
name: 'Ayesha Malik',
email: 'ayesha.malik@example.com',
role: 'editor',
isActive: true
};
That tells you something. It reflects what your app is actually expecting. And when it breaks, the reason will actually make sense.
Stop pasting the same user object into every test. Seriously. Just write a helper.
function makeUser(overrides = {}) {
return {
id: crypto.randomUUID(),
name: 'Default User',
email: 'default@example.com',
role: 'user',
...overrides
};
}
Then you can create exactly what you need, without repeating yourself:
const admin = makeUser({ role: 'admin' });
const inactive = makeUser({ isActive: false });
Clean, simple, and way easier to update when things change.
Big, bloated JSON files full of every possible field? Hard pass. Keep things scoped. If you're testing the dashboard, mock just enough data for that. If you're testing project creation, mock just the project—not five users and three invoices too.
Inline it if it's small. Use a fixture file if multiple tests share it. Just don’t hoard data.
It’s tempting to use a faker library and go wild with randomness. But guess what? Random test data breaks in weird ways. You’ll get a CI failure on Wednesday because one username had a special character you weren’t expecting.
Instead: pick realistic defaults. Use seeded randomness if you need variety. Make your tests consistent and repeatable.
const projectName = `test-project-${Date.now()}`;
That’s fine for local runs. But in CI? Stick with fixed, predictable values unless you're intentionally testing edge cases.
A red flag: your test setup is half a page long, and the thing you're testing is one line at the bottom. That’s a smell.
If your test is about showing a message when a user has no projects, don’t create five users and mock two APIs just to get there. Stub what matters. Skip the rest.
Your test should tell a clear story:
The rest is just noise.
Please. No more data.json or user1, user2, user3. Give your test data intentional names.
const guestUser = makeUser({ role: 'guest' });
const suspendedUser = makeUser({ status: 'suspended' });
Now your test reads like English. You’ll thank yourself next week when you revisit it.
Good test data is like good scaffolding. It’s not flashy, but without it, the whole thing collapses when someone bumps the wall.
Here’s what matters:
You now know how to write real tests, fake what’s flaky, debug what’s broken, and build a test suite that doesn’t fall apart when someone else pushes to main.
That’s huge.
If you ever feel stuck or want a proper walkthrough with actual projects and broken apps you can fix yourself, check out Uncodemy’s Playwright course. No fluff. Just working code, explained clearly.
Now go build something. And if it breaks? You’ll be ready.
Personalized learning paths with interactive materials and progress tracking for optimal learning experience.
Explore LMSCreate professional, ATS-optimized resumes tailored for tech roles with intelligent suggestions.
Build ResumeDetailed analysis of how your resume performs in Applicant Tracking Systems with actionable insights.
Check ResumeAI analyzes your code for efficiency, best practices, and bugs with instant feedback.
Try Code ReviewPractice coding in 20+ languages with our cloud-based compiler that works on any device.
Start Coding
TRENDING
BESTSELLER
BESTSELLER
TRENDING
HOT
BESTSELLER
HOT
BESTSELLER
BESTSELLER
HOT
POPULAR