Parameterized tests (sinh nhiều test từ dữ liệu)
automationplaywright
Parameterized tests là kỹ thuật dùng vòng for/forEach trên mảng dữ liệu để sinh ra nhiều test cùng một kịch bản với input khác nhau — Playwright không có sẵn test.each như Jest nên tự loop. Nó giải quyết data-driven testing: boundary values, nhiều bộ credentials, dữ liệu từ CSV — một dòng data thành một test case riêng trên report thay vì copy-paste.
Khái niệm & Bản chất
- Data-driven testing: 1 kịch bản × N bộ data = N test case độc lập trên report — mỗi case pass/fail riêng, không nuốt lẫn nhau.
- Playwright không có
test.eachnhư Jest — tự dùngfor...of/forEachtrên mảng để khai báo nhiềutest(). - Loop chạy lúc load file (collection phase), không phải runtime — mỗi iteration đăng ký 1 test với data đã bind sẵn vào closure.
- Data nguồn có thể: array inline, import JSON, đọc CSV/Excel bằng
fsở module scope — miễn là đồng bộ lúc file được load. - Mỗi test sinh ra vẫn isolation đầy đủ (context/page mới) — data mutate trong test này không ảnh hưởng test kia.
Cách hoạt động & API chính
- Pattern chuẩn:
for (const tc of testCases) { test('...', async ({ page }) => { /* dùng tc */ }) }— tên test thường là template literal chứa data. - Tên test phải chứa data để phân biệt trên report:
login fail — ${tc.name}hoặc thêm index/id khi data có thể trùng. - Có thể loop
test.describecho nhóm:for (const set of dataSets) { test.describe('Bộ ' + set.name, () => { ... }) }. - Đọc CSV:
fs.readFileSync+ parser (csv-parse/sync, papaparse) ở module scope — phải đồng bộ vì collection không chờ promise. - Kết hợp được với tag/annotation sinh theo data:
{ tag: tc.tag }hoặctest.skip(tc.disabled)— bộ data tự quyết định test nào chạy.
Ví dụ & Cú pháp thực tế
- Inline array — classic login negative cases:
``
ts import { test, expect } from '@playwright/test'; const loginCases = [ { name: 'email sai định dạng', user: 'abc', pass: 'x', msg: 'Email không hợp lệ' }, { name: 'sai mật khẩu', user: 'qa@hoctest.com', pass: 'wrong', msg: 'Sai thông tin đăng nhập' }, { name: 'để trống cả hai', user: '', pass: '', msg: 'Vui lòng nhập đầy đủ' }, ]; for (const tc of loginCases) { test(login fail — ${tc.name}, async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill(tc.user); await page.getByLabel('Mật khẩu').fill(tc.pass); await page.getByRole('button', { name: 'Đăng nhập' }).click(); await expect(page.getByText(tc.msg)).toBeVisible(); }); }`` - CSV-driven — đọc file data bằng csv-parse:
``
ts import fs from 'node:fs'; import { parse } from 'csv-parse/sync'; import { test, expect } from '@playwright/test'; const rows: { username: string; role: string }[] = parse( fs.readFileSync('data/users.csv'), { columns: true, skip_empty_lines: true } ); for (const [i, r] of rows.entries()) { test(tạo user #${i} — ${r.username} (${r.role}), async ({ request }) => { const res = await request.post('/api/users', { data: { username: r.username, role: r.role }, }); expect(res.status()).toBe(201); }); }`` - Sinh tag theo data — một phần suite chạy smoke:
``
ts const cases = [ { name: 'happy path', qty: 1, tag: '@smoke' }, { name: 'số lượng 0', qty: 0, tag: '@regression' }, { name: 'số âm', qty: -1, tag: '@regression' }, ]; for (const tc of cases) { test(mua hàng — ${tc.name}, { tag: tc.tag }, async ({ page }) => { // ... }); }``
Best practices & Khi nào dùng
- Luôn đưa data vào tên test — report fail phải đọc được 'case nào' chết mà không cần mở trace. Thêm index (
#0,#1) khi data có thể trùng. - Ưu tiên param test hơn loop assert trong 1 test: 1 case fail không chặn case khác, report đếm đúng số case, retry chạy lại đúng case hỏng.
- Data nhỏ (vài chục case) → giữ inline trong test file cho dễ review; data lớn → tách
data/*.jsonhoặc CSV, commit kèm. - Case boundary/negative là mỏ vàng cho param test: form validation, phân trang, giới hạn số lượng — 1 kịch bản quét cả bảng equivalence.
- Kết hợp tag theo data (
tc.tag) để bộ data tự phân loại smoke/regression.
Gotchas, So sánh & Lưu ý
- Không
awaitđược data ở top-level để sinh test — collection chạy đồng bộ lúc load file; data phải fetch trước (build script, globalSetup sinh file) hoặc đọc sync bằngfs.readFileSync. Muốn lấy data từ API runtime thì chia 2 phase:beforeAllfetch → 1 test loop assert bên trong (mất lợi ích per-case). - Biến trong
for...ofvớiconstmỗi iteration là closure riêng — an toàn; tránh pattern cũvar(share biến, mọi test nhận giá trị cuối). - Test name trùng nhau → report khó phân biệt và một số reporter merge kết quả — đảm bảo unique bằng index/id/name.
- Bộ data khổng lồ (vài trăm dòng CSV) → số test sinh ra thổi phồng thời gian chạy và report; cân nhắc gom nhóm, sample data, hoặc tách file riêng có project riêng.
- So sánh Jest:
test.each(table)gọn hơn một chút, nhưng loopforcủa Playwright linh hoạt hơn — đặt được tag/annotation/serial theo từng dòng data. - Debug 1 case trong vòng lặp:
test.onlytrong loop sẽ only tất cả test của loop — muốn 1 case thì filter mảng tạm hoặc dùng--greptheo tên case.
