Data-Driven Test Automation With Playwright: Beyond Static Scripts to Dynamic Frameworks
Modern web applications are dynamic. Your tests should be too. Static, hardcoded test scripts fail at scale because every new test scenario requires a new test file. Data-driven testing generates hundreds of test cases from a single test template.
🎠Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
JSON-Based Parameterization
// testdata/login-scenarios.json
[
{ "email": "admin@test.com", "password": "valid-pass", "expected": "dashboard", "desc": "valid admin" },
{ "email": "user@test.com", "password": "wrong", "expected": "error", "desc": "invalid password" },
{ "email": "", "password": "pass", "expected": "error", "desc": "empty email" },
{ "email": "admin@test.com", "password": "", "expected": "error", "desc": "empty password" },
{ "email": "sql'injection", "password": "pass", "expected": "error", "desc": "SQL injection in email" }
]
// tests/login.spec.ts
import scenarios from '../testdata/login-scenarios.json';
for (const scenario of scenarios) {
test('login: ' + scenario.desc, async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(scenario.email, scenario.password);
if (scenario.expected === 'dashboard') {
await expect(page).toHaveURL(/dashboard/);
} else {
await expect(page.getByRole('alert')).toBeVisible();
}
});
}
CSV Data Sources With Fixtures
import { test } from '@playwright/test';
import fs from 'fs';
function loadCsv(path: string): Record<string, string>[] {
const content = fs.readFileSync(path, 'utf-8');
const [header, ...rows] = content.split('\n').filter(Boolean);
const keys = header.split(',');
return rows.map(row => {
const values = row.split(',');
return Object.fromEntries(keys.map((k, i) => [k.trim(), values[i]?.trim()]));
});
}
const products = loadCsv('testdata/products.csv');
for (const product of products) {
test('add to cart: ' + product.name, async ({ page }) => {
await page.goto('/products/' + product.id);
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
}
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
API-Driven Test Data Setup
// fixtures/data.fixture.ts
import { test as base } from '@playwright/test';
type TestData = { userId: string; productId: string; orderId: string };
export const test = base.extend<{ testData: TestData }>({
testData: async ({ request }, use) => {
// Create test data via API
const user = await (await request.post('/api/users', { data: { name: 'Test' } })).json();
const product = await (await request.post('/api/products', { data: { name: 'Widget', price: 29.99 } })).json();
await use({ userId: user.id, productId: product.id, orderId: '' });
// Cleanup
await request.delete('/api/users/' + user.id);
await request.delete('/api/products/' + product.id);
}
});
When to Use Each Approach
| Approach | Best For | Avoid When |
|---|---|---|
| JSON files | Small, static datasets (login scenarios, form validations) | Data changes frequently |
| CSV files | Large datasets, non-technical contributors editing data | Complex nested structures |
| API fixtures | Dynamic data, test isolation, parallel execution | APIs are unstable or slow |
| Database seeding | Complex relational data, performance testing | No direct DB access in CI |
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
