Danh sách bài viết

Bài 91: Parametrize Qua Options Fixture

Bài 90 dùng for...of để sinh nhiều test trong một file. Bài này dùng cách tiếp cận khác: khai báo fixture với option: true (đã giới thiệu cú pháp ở bài 23), sau đó đặt nhiều project trong config, mỗi project truyền giá trị khác nhau qua use:. Kết quả: cùng 1 test chạy N lần — mỗi lần với bộ tham số của 1 project. Bài này tập trung vào ứng dụng kỹ thuật đó vào các bài toán cụ thể: multi-role, multi-env, feature variant, multi-tenant, cùng pattern kết hợp options fixture với computed fixture phụ thuộc.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này, bạn sẽ:

  • Hiểu cơ chế options fixture kết hợp project matrix: 1 test chạy N lần, mỗi lần 1 project.
  • Biết cách áp dụng pattern vào multi-role, multi-env, feature variant và multi-tenant.
  • Kết hợp options fixture với computed fixture phụ thuộc (ví dụ authedPage phụ thuộc userRole).
  • Đọc và lọc kết quả test theo project trong reporter.
  • Nhận biết giới hạn của cách tiếp cận này và biết khi nào dùng forEach thay thế.
  • Tránh 4 pitfall hay gặp.
2

Cơ Chế Hoạt Động

Khi chạy npx playwright test, Playwright đọc danh sách projects trong config. Mỗi project có thể chứa use: { ... } để cung cấp giá trị cho các option fixtures. Playwright lần lượt chạy mỗi test trong mỗi project — nếu có N project, 1 test sẽ xuất hiện N lần trong runner.

Điểm mấu chốt: giá trị option fixture được resolve per-project trước khi test chạy. Test không cần biết đang chạy với project nào — nó chỉ nhận fixture đã được resolve. Toàn bộ "parametrize logic" nằm ở config, không nằm trong file test.

Luồng thực thi với 3 project admin, user, guest:

project "admin"  → resolve userRole = 'admin'  → chạy test
project "user"   → resolve userRole = 'user'   → chạy test
project "guest"  → resolve userRole = 'guest'  → chạy test

Test body hoàn toàn không đổi. Chỉ giá trị fixture thay đổi theo project.

3

Cú Pháp Đầy Đủ

Bài 23 đã cover cú pháp tuple [defaultValue, { option: true }]. Phần này chỉ nhắc lại ngắn gọn trong ngữ cảnh multi-role để tiện theo dõi. Chi tiết về cú pháp xem tại bài 23.

Bước 1 — Khai báo fixture với option: true:

// fixtures.ts
import { test as base, Page } from '@playwright/test';

export const test = base.extend<{ userRole: 'admin' | 'user' | 'guest' }>({
  userRole: ['guest', { option: true }],
  //        ^--- default value   ^--- đánh dấu là option fixture
});

Bước 2 — Khai báo project matrix trong config:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'admin', use: { userRole: 'admin' } },
    { name: 'user',  use: { userRole: 'user'  } },
    { name: 'guest', use: { userRole: 'guest' } },
  ],
});

Bước 3 — Viết test nhận fixture:

// dashboard.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('view dashboard', async ({ page, userRole }) => {
  await page.goto('/dashboard');

  if (userRole === 'admin') {
    await expect(page.getByRole('link', { name: 'Admin Panel' })).toBeVisible();
  } else {
    await expect(page.getByRole('link', { name: 'Admin Panel' })).toBeHidden();
  }
});

Kết quả: test view dashboard chạy 3 lần — một lần với mỗi project. Mỗi lần userRole là giá trị khác nhau do project cung cấp.

4

Khác Biệt Với forEach (Bài 90)

Hai kỹ thuật đều cho phép chạy cùng logic test với nhiều bộ tham số, nhưng cơ chế và phạm vi ứng dụng khác nhau:

forEach (bài 90) Options fixture + project matrix
Nơi khai báo data Inline trong file test (hoặc import) playwright.config.ts
Test count N test trong file × số lần chạy 1 test × N project
Số test trong reporter N dòng riêng với tên khác nhau N dòng với cùng tên, khác nhau ở project prefix
Áp dụng cho toàn suite Không — chỉ cho file khai báo Có — project áp dụng cho mọi test file
Thêm variant mới Thêm entry vào mảng trong file test Thêm project vào config
Phù hợp khi Data nhiều, variant trong 1 run config Variant theo project — multi-browser, multi-role

Ví dụ về số test: có 5 file spec, mỗi file có 10 test, mảng data 3 entries.

  • Với forEach: 50 test × 3 = 150 test entries (test sinh theo file, data inline).
  • Với options fixture + 3 project: 50 test × 3 project = 150 runs (tổng lần chạy giống nhau, nhưng cấu trúc tổ chức khác).

Sự khác biệt thực sự nằm ở nơi kiểm soát variant: forEach để dev kiểm soát trong file test, options fixture để tổ chức kiểm soát trong config — có thể thêm hoặc bỏ project mà không chạm vào test code.

5

Use Case: Multi-Role Test

Đây là use case điển hình nhất. Cùng một test flow — ví dụ "xem dashboard" — cần chạy với nhiều role khác nhau để kiểm tra từng role thấy đúng UI của mình. Thay vì viết 3 test riêng hoặc dùng forEach, khai báo userRole là option fixture và để project matrix xử lý phần còn lại.

// fixtures.ts
export const test = base.extend<{ userRole: 'admin' | 'user' | 'guest' }>({
  userRole: ['guest', { option: true }],
});

// playwright.config.ts
projects: [
  { name: 'admin', use: { userRole: 'admin' } },
  { name: 'user',  use: { userRole: 'user'  } },
  { name: 'guest', use: { userRole: 'guest' } },
],

// ui-permissions.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';

test('nav items visibility', async ({ page, userRole }) => {
  await page.goto('/');
  const nav = page.getByRole('navigation');

  // Admin thấy thêm Settings và User Management
  if (userRole === 'admin') {
    await expect(nav.getByRole('link', { name: 'Settings' })).toBeVisible();
    await expect(nav.getByRole('link', { name: 'User Management' })).toBeVisible();
  } else {
    await expect(nav.getByRole('link', { name: 'Settings' })).toBeHidden();
  }

  // Tất cả role đều thấy Dashboard
  await expect(nav.getByRole('link', { name: 'Dashboard' })).toBeVisible();
});

test('delete button visibility', async ({ page, userRole }) => {
  await page.goto('/posts/1');
  const deleteBtn = page.getByRole('button', { name: 'Delete' });

  if (userRole === 'admin') {
    await expect(deleteBtn).toBeVisible();
  } else {
    await expect(deleteBtn).toBeHidden();
  }
});

2 test file × 3 project = 6 lần chạy. Mỗi lần userRole có giá trị phù hợp với project. Khi thêm role mới (ví dụ moderator), chỉ cần thêm 1 project vào config và cập nhật logic trong test — không cần sửa cấu trúc vòng lặp hay data array.

6

Use Case: Multi-Env

Chạy cùng bộ test trên dev, staging, và production URL — không cần 3 config file riêng. Khai báo apiBaseURL là option fixture (xem bài 23 cho chi tiết), rồi tạo 3 project tương ứng.

// playwright.config.ts
projects: [
  {
    name: 'dev',
    use: { apiBaseURL: 'https://api.dev.internal' },
    testMatch: /.*\.spec\.ts/,
  },
  {
    name: 'staging',
    use: { apiBaseURL: 'https://api.staging.example.com' },
    testMatch: /.*\.spec\.ts/,
  },
  {
    name: 'prod',
    use: { apiBaseURL: 'https://api.example.com' },
    testMatch: /smoke\.spec\.ts/,  // chỉ smoke test cho prod
  },
],

Dùng testMatch để giới hạn test nào chạy trên prod — không muốn chạy toàn bộ suite trên production.

Trong CI, có thể chọn chạy một project cụ thể tuỳ theo branch:

# CI: chỉ chạy project staging
npx playwright test --project=staging
7

Use Case: Feature Variant

Khi ứng dụng đang chạy A/B test hoặc feature flag, dùng options fixture để test cả hai nhánh trong cùng một suite run.

// fixtures.ts
export const test = base.extend<{
  checkoutVersion: 'legacy' | 'v2';
}>({
  checkoutVersion: ['legacy', { option: true }],
});

// playwright.config.ts
projects: [
  { name: 'checkout-legacy', use: { checkoutVersion: 'legacy' } },
  { name: 'checkout-v2',     use: { checkoutVersion: 'v2'     } },
],

// checkout.spec.ts
test('complete purchase', async ({ page, checkoutVersion }) => {
  await page.goto('/cart');

  if (checkoutVersion === 'v2') {
    // v2 dùng single-page checkout
    await page.getByTestId('checkout-v2-btn').click();
    await page.getByLabel('Card number').fill('4111111111111111');
    await page.getByRole('button', { name: 'Pay now' }).click();
  } else {
    // legacy checkout flow 3 bước
    await page.getByRole('button', { name: 'Proceed to checkout' }).click();
    await page.getByRole('button', { name: 'Continue to payment' }).click();
    await page.getByRole('button', { name: 'Place order' }).click();
  }

  await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});

Cả hai variant đều phải reach Order confirmed — test verify cả hai nhánh đi đến cùng kết quả. Nếu v2 broken, chỉ checkout-v2 project fail, không ảnh hưởng legacy.

8

Use Case: Multi-Tenant

SaaS multi-tenant: mỗi tenant có subdomain, branding, và config riêng. Cùng test kiểm tra login, dashboard, billing — nhưng với data và URL của từng tenant.

// fixtures.ts
export type TenantConfig = {
  baseURL: string;
  tenantId: string;
  adminEmail: string;
};

export const test = base.extend<{ tenantConfig: TenantConfig }>({
  tenantConfig: [
    { baseURL: 'https://acme.app.local', tenantId: 'acme', adminEmail: '[email protected]' },
    { option: true },
  ],
});

// playwright.config.ts
projects: [
  {
    name: 'tenant-acme',
    use: {
      tenantConfig: {
        baseURL: 'https://acme.app.local',
        tenantId: 'acme',
        adminEmail: '[email protected]',
      },
    },
  },
  {
    name: 'tenant-globex',
    use: {
      tenantConfig: {
        baseURL: 'https://globex.app.local',
        tenantId: 'globex',
        adminEmail: '[email protected]',
      },
    },
  },
],

// tenant-smoke.spec.ts
test('admin login', async ({ page, tenantConfig }) => {
  await page.goto(`${tenantConfig.baseURL}/login`);
  await page.getByLabel('Email').fill(tenantConfig.adminEmail);
  await page.getByLabel('Password').fill('test-password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL(/dashboard/);
});

Thêm tenant mới chỉ cần thêm 1 entry vào projects — không sửa test.

9

Combine Options Fixture Với Computed Fixture

Options fixture thường có giá trị nguyên thủy (string, number, boolean). Khi cần fixture phức tạp hơn — như một Page đã được login — khai báo một fixture thứ hai phụ thuộc vào options fixture. Fixture thứ hai tự động nhận giá trị đã resolve của options fixture.

// fixtures.ts
import { test as base, Page } from '@playwright/test';

type Fixtures = {
  userRole: 'admin' | 'user' | 'guest';
  authedPage: Page;
};

export const test = base.extend<Fixtures>({
  userRole: ['guest', { option: true }],

  authedPage: async ({ page, userRole }, use) => {
    // Computed fixture — phụ thuộc vào userRole đã được resolve
    if (userRole !== 'guest') {
      await loginAs(page, userRole);
    }
    await use(page);
    // Teardown tự động khi test xong (page đóng theo context)
  },
});

// helper
async function loginAs(page: Page, role: string) {
  await page.goto('/login');
  await page.getByLabel('Email').fill(`${role}@example.test`);
  await page.getByLabel('Password').fill('test-password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');
}

Sử dụng trong test:

test('view profile page', async ({ authedPage, userRole }) => {
  await authedPage.goto('/profile');

  // authedPage đã login đúng role
  if (userRole === 'guest') {
    // Guest không login — bị redirect
    await expect(authedPage).toHaveURL(/login/);
  } else {
    await expect(authedPage.getByRole('heading', { name: 'My Profile' })).toBeVisible();
  }
});

Điểm mấu chốt: authedPage không cần biết userRole là gì lúc compile. Playwright resolve userRole từ project config trước, rồi inject vào authedPage setup function. Khi project admin chạy, authedPage nhận userRole = 'admin' và tự login bằng admin credentials.

Pattern này tách biệt rõ hai concern:

  • Options fixture (userRole) — giá trị cấu hình, đến từ project config.
  • Computed fixture (authedPage) — hành vi phụ thuộc, đóng gói logic setup phức tạp.
10

Reporter Output

Playwright reporter phân biệt các lần chạy bằng project name — hiển thị trước tên test dưới dạng prefix trong ngoặc vuông:

  [admin] › ui-permissions.spec.ts:5:1 › nav items visibility
  [user]  › ui-permissions.spec.ts:5:1 › nav items visibility
  [guest] › ui-permissions.spec.ts:5:1 › nav items visibility
  [admin] › ui-permissions.spec.ts:20:1 › delete button visibility
  [user]  › ui-permissions.spec.ts:20:1 › delete button visibility
  [guest] › ui-permissions.spec.ts:20:1 › delete button visibility

Khi một trong số các lần chạy fail, reporter hiển thị rõ project nào fail:

  1) [user] › ui-permissions.spec.ts:5:1 › nav items visibility ─────────────────

    Error: expect(locator).toBeHidden() failed.
    Locator: getByRole('link', { name: 'Settings' })

    Expected: hidden
    Received: visible

Từ output trên đọc được ngay: project user fail tại test nav items visibility — link Settings không bị ẩn với role user như mong đợi. Không cần lục lại code để biết lần chạy nào bị vỡ.

HTML reporter tổng hợp kết quả per-project vào tab riêng, có thể filter theo project name.

11

Filter Chạy Một Subset

Khi debug, thường chỉ cần chạy 1 project thay vì toàn bộ matrix. Dùng flag --project:

# Chỉ chạy project admin
npx playwright test --project=admin

# Chỉ chạy project admin và user (nhiều project)
npx playwright test --project=admin --project=user

# Kết hợp với --grep để lọc thêm theo tên test
npx playwright test --project=admin --grep="nav items"

Tên project phải khớp chính xác (case-sensitive) với tên khai báo trong playwright.config.ts. Nếu project name có khoảng trắng, đặt trong ngoặc kép:

npx playwright test --project="checkout-v2"

Liệt kê tất cả test sẽ chạy (không chạy thực sự) với một project:

npx playwright test --project=admin --list
12

Multi-Dimension Matrix

Có thể kết hợp nhiều options fixture trong cùng một project để tạo matrix nhiều chiều. Ví dụ: browser × role.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium-admin',
      use: { ...devices['Desktop Chrome'], userRole: 'admin' },
    },
    {
      name: 'chromium-user',
      use: { ...devices['Desktop Chrome'], userRole: 'user' },
    },
    {
      name: 'firefox-admin',
      use: { ...devices['Desktop Firefox'], userRole: 'admin' },
    },
    {
      name: 'firefox-user',
      use: { ...devices['Desktop Firefox'], userRole: 'user' },
    },
  ],
});

4 project tạo ra 4 × (số test) lần chạy. Với 10 test: 40 lần chạy. Matrix 3 chiều (browser × role × env) với 3 × 3 × 3 = 27 project sẽ tạo ra 270 lần chạy cho 10 test — CI time tăng tuyến tính.

Thực tế: không phải mọi tổ hợp đều cần test. Cân nhắc:

  • Chỉ tạo project cho các tổ hợp có ý nghĩa kinh doanh.
  • Phân tách matrix lớn thành nhiều config file (dùng --config flag) để chạy song song trên nhiều CI runner.
  • Dùng testMatch per-project để giới hạn test file nào áp dụng cho project nào.

Bài 92 deep dive vào project matrix — cách tổ chức, tối ưu, và manage số lượng project lớn.

13

Limitations

1. Thêm variant cần sửa config

Mỗi lần thêm variant mới phải thêm project vào playwright.config.ts. Khác với forEach, developer không thể tự thêm data vào file test — cần quyền chỉnh sửa config (hoặc CI pipeline config). Với team lớn, đây đôi khi là điểm nghẽn.

2. Số project lớn làm CI kéo dài

Mỗi project chạy toàn bộ test suite. Matrix 5 browser × 4 role × 3 env = 60 project — tăng 60 lần so với chạy không có matrix. Cần tính toán kỹ trước khi mở rộng matrix.

3. Không thể parametrize per-test từ project config

Options fixture là per-project — tất cả test trong project nhận cùng một giá trị. Không thể có project admin mà chỉ áp dụng fixture cho 1 test cụ thể. Nếu cần per-test override, dùng test.use() trong test.describe() block (xem bài 23).

4. Default value chỉ là giá trị sync

Tuple [defaultValue, { option: true }] không nhận Promise làm default. Nếu cần async initialization, cần computed fixture phụ thuộc vào options fixture — không thể đặt async logic trực tiếp vào default value của tuple.

14

Pitfalls

Pitfall 1 — Quên option: true khi khai báo fixture

Khai báo fixture dưới dạng function fixture thông thường nhưng cố override qua use: trong project config — giá trị bị bỏ qua lặng lẽ, test vẫn chạy với giá trị từ hàm setup của fixture, không phải giá trị từ project.

// SAI — không có option: true
export const test = base.extend<{ userRole: string }>({
  userRole: async ({}, use) => {
    await use('guest'); // luôn là 'guest', không override được
  },
});

// playwright.config.ts
use: { userRole: 'admin' }, // BỊ BỎ QUA — không có error, không có warning

// ĐÚNG
userRole: ['guest', { option: true }],

Playwright không báo lỗi hay warning khi project use: chứa key không match options fixture. Triệu chứng: tất cả project chạy với cùng một giá trị (giá trị hardcode trong fixture), không phân biệt nhau.

Pitfall 2 — Default value không hợp lý gây test fail khi không set

Nếu default value là undefined hoặc giá trị không hợp lệ, test chạy mà không có project override sẽ fail theo cách khó debug.

// Không tốt — undefined làm default
userRole: [undefined as unknown as 'admin' | 'user' | 'guest', { option: true }],

// Trong test:
if (userRole === 'admin') { ... }
// → Nhánh nào cũng không match → test không kiểm tra gì

// Tốt hơn: đặt default hợp lý nhất cho "unset" case
userRole: ['guest', { option: true }],  // guest là role ít quyền nhất — safe default

Nếu fixture phải được set bởi project (không có default hợp lý), dùng function fixture và throw lỗi rõ ràng khi không được cung cấp, thay vì dùng tuple với undefined.

Pitfall 3 — Nhầm options fixture (project-level) với forEach (file-level)

Hai kỹ thuật có cú pháp khác nhau nhưng cùng mục đích "chạy nhiều lần". Nhầm lẫn thường dẫn đến: viết forEach trong file test thay vì khai báo project, hoặc khai báo nhiều project không cần thiết thay vì forEach đơn giản.

Rule ngắn gọn: nếu variant cần áp dụng cho toàn bộ suite và được kiểm soát từ config (hoặc CI) → dùng options fixture + project. Nếu variant chỉ cần cho 1 file hoặc 1 nhóm test cụ thể → dùng forEach.

Pitfall 4 — Project matrix lớn làm CI explosion

// 3 browser × 4 role × 3 env = 36 project
// Với 50 test: 1,800 lần chạy
// Với timeout 30s/test: 15 giờ nếu chạy tuần tự

// Giảm số project bằng testMatch — chỉ smoke test cho prod × multi-browser
{ name: 'chromium-admin-prod', use: { ...devices['Desktop Chrome'], userRole: 'admin' }, testMatch: /smoke/ },
{ name: 'chromium-user-prod',  use: { ...devices['Desktop Chrome'], userRole: 'user'  }, testMatch: /smoke/ },
// Đầy đủ hơn chỉ cho dev
{ name: 'chromium-admin-dev', use: { ...devices['Desktop Chrome'], userRole: 'admin' } },
{ name: 'chromium-user-dev',  use: { ...devices['Desktop Chrome'], userRole: 'user'  } },
// ...

Ngoài ra, Playwright hỗ trợ workers config để chạy parallel — nhưng CI runner vẫn bị giới hạn bởi số core và memory. Với matrix lớn, cân nhắc sharding (xem bài về sharding) để phân phối sang nhiều CI runner.

15

Quiz

Câu 1. Có 3 project (admin, user, guest) và 1 spec file với 5 test. Fixture userRole khai báo với option: true. Khi chạy npx playwright test (không có flag nào thêm), có bao nhiêu lần test thực sự chạy?

Đáp án

15 lần — 5 test × 3 project. Mỗi test chạy một lần cho mỗi project. Reporter hiển thị 15 entries, mỗi entry có project name prefix.

Câu 2. Code sau có vấn đề gì? Hành vi thực tế khi chạy?

// fixtures.ts
export const test = base.extend<{ env: string }>({
  env: async ({}, use) => {
    await use('dev');
  },
});

// playwright.config.ts
projects: [
  { name: 'dev',     use: { env: 'dev'     } },
  { name: 'staging', use: { env: 'staging' } },
],
Đáp án

Fixture env khai báo là function fixture thông thường, không có option: true. Giá trị use: { env: 'staging' } trong project config bị bỏ qua lặng lẽ. Cả hai project đều chạy với env = 'dev' — không có error, không có warning, nhưng staging không thực sự test với env staging.

Fix: thay bằng env: ['dev', { option: true }].

Câu 3. Khi nào nên dùng options fixture + project matrix thay vì forEach? Cho ví dụ tình huống cụ thể mỗi cách.

Đáp án

Dùng options fixture + project matrix khi: variant cần áp dụng cho toàn bộ suite, được kiểm soát từ config hoặc CI pipeline, thêm variant không cần sửa test code. Ví dụ: chạy toàn bộ suite với 3 role (admin/user/guest) — thêm role mới chỉ cần thêm project.

Dùng forEach khi: data variant chỉ liên quan đến 1 test hoặc 1 file, variant cần được định nghĩa gần code logic, thêm case không cần config change. Ví dụ: test form validation với 10 bộ input khác nhau — data phù hợp đặt ngay trong file test.

Câu 4. Fixture authedPage phụ thuộc vào options fixture userRole. Khi project admin chạy, Playwright xử lý dependency như thế nào?

Đáp án

Playwright resolve dependency theo thứ tự: đọc use: { userRole: 'admin' } từ project config → resolve userRole = 'admin' (options fixture nhận giá trị từ project) → khi test yêu cầu authedPage, gọi setup function của authedPage với userRole = 'admin' đã resolved. authedPage setup function thấy userRole = 'admin' và thực hiện login bằng admin credentials.

Câu 5. Config sau có bao nhiêu tổ hợp (project)? Khi chạy với 8 test, có bao nhiêu lần test thực sự chạy?

projects: [
  { name: 'chromium-admin',   use: { ...devices['Desktop Chrome'],  userRole: 'admin' } },
  { name: 'chromium-user',    use: { ...devices['Desktop Chrome'],  userRole: 'user'  } },
  { name: 'firefox-admin',    use: { ...devices['Desktop Firefox'], userRole: 'admin' } },
  { name: 'firefox-user',     use: { ...devices['Desktop Firefox'], userRole: 'user'  } },
  { name: 'webkit-admin',     use: { ...devices['Desktop Safari'],  userRole: 'admin' } },
  { name: 'webkit-user',      use: { ...devices['Desktop Safari'],  userRole: 'user'  } },
],
Đáp án

6 project (3 browser × 2 role). Với 8 test: 8 × 6 = 48 lần chạy. Reporter hiển thị 48 entries. Để chỉ chạy Chromium: npx playwright test --project=chromium-admin --project=chromium-user → 16 lần chạy.

16

Bài Tiếp Theo

Bài 92: Project Matrix — deep dive cách tổ chức, quản lý và tối ưu project matrix lớn: kế thừa project, conditionally enable project từ environment variable, sharding để phân phối CI load.