🎭 Playwright Test Organization: Building Your Testing Kingdom
Imagine you’re building a LEGO castle. You don’t just dump all pieces on the floor and hope for the best. You organize them into groups, follow steps in order, and make sure everything is ready before you start. That’s exactly what Playwright test organization does for your code!
🏰 The Castle Analogy
Think of your test suite as a magical castle:
- Test files = Different rooms in the castle
- test.describe = Grouping rooms by purpose (bedrooms, kitchens)
- Hooks = Castle servants who prepare and clean rooms
- test.step = Step-by-step instructions in a recipe book
Let’s explore each room!
📁 Test File Structure
Your test files are like different buildings in your castle town.
Where Do Tests Live?
my-project/
├── tests/
│ ├── login.spec.ts
│ ├── cart.spec.ts
│ └── checkout.spec.ts
└── playwright.config.ts
Simple Rules:
- Put tests in a
testsfolder - Name files with
.spec.tsor.test.ts - One file = One feature (login, cart, etc.)
Think of it like this: You wouldn’t put your toys AND your clothes in the same drawer. Tests for login go in login file!
✨ The test Function: Your Basic Building Block
Every test starts with the test function. It’s like saying: “Here’s one thing I want to check!”
import { test, expect } from '@playwright/test';
test('page has title', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle('Example');
});
Breaking It Down:
| Part | What It Does |
|---|---|
test |
Tells Playwright “this is a test” |
'page has title' |
Name of your test (be descriptive!) |
async ({ page }) |
Gets a fresh browser page |
Inside { } |
Your test steps |
Like a recipe: “Test that cake is sweet” → Open box → Taste cake → Check if sweet!
📦 test.describe: Grouping Related Tests
test.describe is like putting related toys in the same toy box.
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test('shows login form', async ({ page }) => {
await page.goto('/login');
await expect(page.locator('form')).toBeVisible();
});
test('shows error for wrong password', async ({ page }) => {
await page.goto('/login');
await page.fill('#password', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toBeVisible();
});
});
Why Group Tests?
- Easy to find: All login tests together
- Shared setup: Hooks work for the whole group
- Better reports: See results by feature
graph TD A["test.describe: Login"] --> B["test: shows form"] A --> C["test: wrong password error"] A --> D["test: successful login"]
🎬 test.beforeAll: The Opening Ceremony
beforeAll runs ONCE before ALL tests in a group start. Like a chef preparing ingredients before cooking multiple dishes.
import { test, expect } from '@playwright/test';
test.describe('Shopping Cart', () => {
test.beforeAll(async () => {
console.log('Setting up test database...');
// This runs ONCE before any test
});
test('can add item', async ({ page }) => {
// This test runs after beforeAll
});
test('can remove item', async ({ page }) => {
// beforeAll does NOT run again
});
});
Perfect For:
- Starting a test database
- Creating test users
- Loading shared test data
Real World: Before a school day, the janitor unlocks ALL doors once. Not before each class!
🎭 test.afterAll: The Closing Ceremony
afterAll runs ONCE after ALL tests finish. Like cleaning up after a party.
import { test } from '@playwright/test';
test.describe('User Tests', () => {
test.afterAll(async () => {
console.log('Cleaning up test data...');
// Delete test users, close connections
});
test('create user', async ({ page }) => {
// test runs
});
test('delete user', async ({ page }) => {
// test runs, THEN afterAll runs
});
});
Perfect For:
- Deleting test data
- Closing database connections
- Generating reports
🔄 test.beforeEach: Fresh Start Every Time
beforeEach runs before EVERY single test. Like making your bed every morning.
import { test, expect } from '@playwright/test';
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// This runs before EACH test
await page.goto('/dashboard');
await page.fill('#username', 'testuser');
await page.fill('#password', 'secret');
await page.click('#login');
});
test('shows welcome message', async ({ page }) => {
// Already logged in!
await expect(page.locator('.welcome')).toBeVisible();
});
test('shows user stats', async ({ page }) => {
// Also already logged in!
await expect(page.locator('.stats')).toBeVisible();
});
});
The Magic:
graph TD A["beforeEach: Login"] --> B["Test 1: welcome"] C["beforeEach: Login"] --> D["Test 2: stats"] E["beforeEach: Login"] --> F["Test 3: settings"]
No repeated code! Login once in beforeEach, use everywhere.
🧹 test.afterEach: Clean Slate After Every Test
afterEach runs after EVERY single test. Like washing your plate after each meal.
import { test } from '@playwright/test';
test.describe('File Upload', () => {
test.afterEach(async ({ page }) => {
// Runs after EACH test
console.log('Clearing uploaded files...');
// Clean up any files created during test
});
test('upload image', async ({ page }) => {
// Upload an image
// afterEach cleans it up!
});
test('upload document', async ({ page }) => {
// Upload a document
// afterEach cleans it up too!
});
});
Perfect For:
- Clearing cookies/storage
- Deleting files created during test
- Taking screenshots on failure
👣 test.step: Breaking Tests into Clear Steps
test.step makes your tests tell a story. Each step is like a chapter in a book.
import { test, expect } from '@playwright/test';
test('complete checkout', async ({ page }) => {
await test.step('Go to shop', async () => {
await page.goto('/shop');
});
await test.step('Add item to cart', async () => {
await page.click('.product');
await page.click('.add-to-cart');
});
await test.step('Fill shipping info', async () => {
await page.fill('#address', '123 Main St');
await page.fill('#city', 'New York');
});
await test.step('Complete payment', async () => {
await page.fill('#card', '4111111111111111');
await page.click('#pay');
await expect(page.locator('.success')).toBeVisible();
});
});
Why Use Steps?
| Benefit | Example |
|---|---|
| Clear Reports | See exactly where test failed |
| Documentation | Test reads like instructions |
| Debugging | Find problems faster |
Think of it: Instead of “make dinner”, you have:
- Chop vegetables
- Boil water
- Add pasta
- Serve!
🔁 The Complete Lifecycle
Here’s how ALL hooks work together:
graph TD A["beforeAll"] --> B["beforeEach"] B --> C["Test 1"] C --> D["afterEach"] D --> E["beforeEach"] E --> F["Test 2"] F --> G["afterEach"] G --> H["afterAll"]
Real Example:
import { test, expect } from '@playwright/test';
test.describe('Blog Posts', () => {
test.beforeAll(async () => {
console.log('🚀 Starting: Create database');
});
test.beforeEach(async ({ page }) => {
console.log('📝 Before test: Login');
await page.goto('/login');
});
test('create post', async ({ page }) => {
await test.step('Click new post', async () => {
await page.click('.new-post');
});
await test.step('Write content', async () => {
await page.fill('.editor', 'Hello World!');
});
});
test.afterEach(async () => {
console.log('🧹 After test: Clear session');
});
test.afterAll(async () => {
console.log('🏁 Finished: Close database');
});
});
🎯 Quick Reference
| Hook | Runs | Use For |
|---|---|---|
beforeAll |
Once before all tests | Setup database, create users |
afterAll |
Once after all tests | Cleanup, close connections |
beforeEach |
Before each test | Login, navigate to page |
afterEach |
After each test | Clear data, take screenshots |
test.step |
Inside test | Organize test into chapters |
🌟 You Did It!
You now understand how to organize Playwright tests like a pro! Remember:
- Files = Separate features
- describe = Group related tests
- Hooks = Setup and cleanup helpers
- Steps = Tell a clear story
Your tests will be clean, organized, and easy to understand. Just like a well-built LEGO castle! 🏰
Next time you write a test, ask yourself: “Where does this belong? What needs to happen before? What needs to happen after?” Your future self will thank you!
