Danh sách bài viết

Bài 56: Setup Project Pattern — Auth Một Lần Cho Cả Suite

Setup project pattern dùng một project đặc biệt — chỉ chạy file *.setup.ts — để login và lưu storageState trước khi các main project khởi động. Main projects khai báo dependencies: ['setup'] nên auth file luôn ready. Bài này tập trung vào cú pháp config, flow thực thi, multi-role auth, so sánh với legacy globalSetup, setup + multi-browser matrix, run commands, 4 pitfall và quiz 5 câu.

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

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

Sau khi hoàn thành bài này, bạn sẽ:

  • Cấu hình setup project trong projects[] với testMatch khớp file *.setup.ts.
  • Khai báo dependencies: ['setup'] trên main project để đảm bảo auth file sẵn sàng trước.
  • Viết auth.setup.ts login qua UI rồi gọi page.context().storageState({ path }).
  • Hiểu sự khác biệt giữa setup project và legacy globalSetup.
  • Xây dựng multi-role auth pattern với nhiều setup project song song.
  • Kết hợp setup project với multi-browser matrix (setup 1 lần, main projects 3 engine).
  • Dùng đúng run commands: chạy cả suite, chỉ main, chỉ setup.
  • Tránh 4 pitfall: quên save state, testMatch sai, thiếu dependencies, token expire.
2

Vấn Đề Cần Giải Quyết

Series 1 (bài 308–310) đã giải thích storageState là snapshot cookies + localStorage của một BrowserContext — load lại để bỏ re-login. Cơ chế đó hoạt động tốt khi bạn có một auth file cố định được tạo thủ công hoặc qua globalSetup.

Vấn đề xuất hiện khi mở rộng suite:

  • Password hoặc token thay đổi — auth file cũ không còn hợp lệ, cần regenerate nhưng không có cơ chế tự động.
  • Multi-role — cần auth file riêng cho admin, user, moderator; globalSetup single-file khó tổ chức.
  • CI không có file auth cũ — mỗi run CI bắt đầu từ clean state, phải login lại trước khi main tests chạy.
  • Visibility trong reportglobalSetup chạy ngoài reporter, fail bí ẩn khó debug trên CI.

Setup project pattern giải quyết bằng cách đưa auth step vào chính pipeline test runner, với đầy đủ retry, timeout, và visibility trong report.

3

Cú Pháp Config — Setup Project + Dependencies

Cấu trúc cơ bản trong playwright.config.ts:

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

export default defineConfig({
  projects: [
    // Setup phase — chỉ chạy file *.setup.ts
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },

    // Main test phase — chờ setup xong mới chạy
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

Hai điểm then chốt:

  • testMatch: /.*\.setup\.ts/ — setup project chỉ nhặt file có đuôi .setup.ts. Main project mặc định dùng testDir và chạy tất cả *.spec.ts. Hai tập file hoàn toàn tách biệt.
  • dependencies: ['setup'] — test runner đảm bảo project 'setup' hoàn thành (tất cả tests pass) trước khi bắt đầu project 'chromium'. Giá trị là array tên project, không phải tên file.

Field storageState trong use của main project trỏ đến file JSON mà setup sẽ tạo ra. Mỗi context mới của main project tự động load file này — mọi test trong chromium project đều bắt đầu đã authenticated, không cần gọi login thêm.

4

Setup File — auth.setup.ts

File setup là một test file bình thường, chỉ khác ở import alias và mục đích:

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.waitForURL('/dashboard');

  // Xác nhận login thành công trước khi save
  await expect(page).toHaveURL('/dashboard');

  // Save storage state ra file
  await page.context().storageState({ path: authFile });
});

Một số điểm cần lưu ý trong file này:

  • Import alias test as setup — không bắt buộc nhưng giúp code dễ đọc hơn; setup('authenticate', ...) rõ ràng hơn test('authenticate', ...).
  • Assertion trước khi saveawait expect(page).toHaveURL('/dashboard') đảm bảo test fail ngay nếu login broken, thay vì save một auth file không hợp lệ rồi để main tests fail muộn hơn với lỗi khó hiểu.
  • page.context().storageState({ path }) — lấy state từ context của page hiện tại và ghi ra file. Đây là bước không thể thiếu — nếu quên dòng này, file user.json không được tạo hoặc nội dung cũ.
  • Thư mục playwright/.auth/ — cần tồn tại trước khi ghi. Có thể tạo tự động bằng fs.mkdirSync ở đầu setup, hoặc commit thư mục rỗng kèm .gitkeep.

Nếu app dùng token-based auth (JWT trong header thay vì cookie), bước login vẫn giữ nguyên — miễn là sau khi login, browser lưu token vào localStorage hoặc cookie, storageState sẽ capture đủ.

5

Flow Thực Thi

Khi chạy npx playwright test, trình tự thực hiện:

  1. Test runner đọc playwright.config.ts, xây dựng dependency graph. Project 'chromium' depends on 'setup' → setup phải complete trước.
  2. Setup project khởi động, chạy auth.setup.ts. Test 'authenticate' thực hiện login UI → page.waitForURL('/dashboard')storageState({ path: 'playwright/.auth/user.json' }).
  3. Setup project hoàn thành (exit code 0). File playwright/.auth/user.json tồn tại trên disk.
  4. Main projects (chromium) khởi động. Mỗi worker tạo BrowserContext mới với storageState: 'playwright/.auth/user.json' — context đã có cookies + localStorage từ bước login.
  5. Mọi test trong main project bắt đầu đã authenticated. Không test nào gặp trang login.

Quan trọng: bước 3 và 4 giao tiếp qua file system, không qua memory. Setup project và main projects là các process riêng — chúng không share variable JavaScript. File JSON trên disk là cách duy nhất để transfer auth state.

6

So Sánh Với Legacy globalSetup

Trước khi có setup project, cách phổ biến là dùng globalSetup trong config:

// Cách cũ — legacy globalSetup
export default defineConfig({
  globalSetup: require.resolve('./global-setup.ts'),
  use: { storageState: 'playwright/.auth/user.json' },
});

So sánh chi tiết:

Tiêu chí globalSetup Setup project
Hiển thị trong report Không Có — là project thực sự
Retry khi fail Không Có — theo retries config
Timeout riêng Không (node process timeout) Có — timeout per test
Multi-role Thủ công trong 1 file Nhiều setup project riêng
Dùng Playwright API Cần chromium.launch() thủ công Dùng fixture page bình thường
Debug fail trên CI Chỉ có stdout log Trace, screenshot, video đầy đủ

Playwright docs (v1.31+) chính thức recommend migrate từ globalSetup sang setup project. globalSetup vẫn hoạt động nhưng không nhận tính năng mới. Bài A.11 trong phần Appendix đề cập đến migration path chi tiết — bài này không deep dive.

7

Multi-Role Auth Pattern

Khi app có nhiều role (admin, user thường, moderator), mỗi role cần một auth file riêng. Pattern: tạo một setup project per role, mỗi main project depend vào setup tương ứng:

projects: [
  // Setup phase — hai setup project chạy song song
  {
    name: 'setup-admin',
    testMatch: /admin\.setup\.ts/,
  },
  {
    name: 'setup-user',
    testMatch: /user\.setup\.ts/,
  },

  // Main test phase — mỗi project dùng auth file của role mình
  {
    name: 'admin-tests',
    use: { storageState: 'playwright/.auth/admin.json' },
    dependencies: ['setup-admin'],
    testMatch: /admin\..*\.spec\.ts/,
  },
  {
    name: 'user-tests',
    use: { storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup-user'],
    testMatch: /user\..*\.spec\.ts/,
  },
]

File setup tương ứng:

// tests/admin.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('authenticate as admin', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('admin-secret');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.waitForURL('/admin/dashboard');
  await expect(page).toHaveURL('/admin/dashboard');
  await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});
// tests/user.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('authenticate as user', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('user-secret');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.waitForURL('/dashboard');
  await expect(page).toHaveURL('/dashboard');
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

Hai setup project 'setup-admin''setup-user' không depend vào nhau, nên test runner có thể chạy song song tùy số worker khả dụng. Admin tests và user tests cũng chạy song song sau khi setup xong — không setup nào block setup kia.

Nếu có test cần kiểm tra tương tác giữa hai role (ví dụ admin xem activity của user), project đó có thể depend vào cả hai setup:

{
  name: 'cross-role-tests',
  // Project này không set storageState ở đây
  // Mỗi test tự load context tương ứng bằng fixture tùy chỉnh
  dependencies: ['setup-admin', 'setup-user'],
  testMatch: /cross-role\..*\.spec\.ts/,
}
8

Setup + Multi-Browser Matrix

Kết hợp setup project với multi-browser matrix từ bài 54–55. Setup chỉ cần chạy 1 lần trên Chromium để tạo auth file — ba main project Chromium, Firefox, WebKit đều dùng chung file đó:

projects: [
  // Setup — chỉ cần 1 browser, Chromium là đủ
  {
    name: 'setup',
    testMatch: /.*\.setup\.ts/,
    use: { ...devices['Desktop Chrome'] },
  },

  // Main — 3 browser, đều depend vào cùng setup
  {
    name: 'chromium',
    dependencies: ['setup'],
    use: {
      ...devices['Desktop Chrome'],
      storageState: 'playwright/.auth/user.json',
    },
  },
  {
    name: 'firefox',
    dependencies: ['setup'],
    use: {
      ...devices['Desktop Firefox'],
      storageState: 'playwright/.auth/user.json',
    },
  },
  {
    name: 'webkit',
    dependencies: ['setup'],
    use: {
      ...devices['Desktop Safari'],
      storageState: 'playwright/.auth/user.json',
    },
  },
],

Tại sao setup không cần chạy trên cả 3 browser? Auth file chứa cookies và localStorage — những giá trị này không phụ thuộc browser engine. Cookie session_id set bởi server là giống nhau dù login từ Chrome hay Firefox. Load storageState từ file Chromium vào Firefox context là hoàn toàn hợp lệ.

Tuy nhiên có một ngoại lệ: nếu app có login flow khác nhau giữa browser (ví dụ sử dụng WebAuthn hay Browser-specific API), cần setup riêng per browser. Với auth dạng username/password thông thường, 1 setup file là đủ cho cả 3 browser.

9

Run Commands

Ba scenario run phổ biến:

# Chạy setup + main (mặc định — CI dùng lệnh này)
npx playwright test

# Chỉ chạy main, bỏ qua setup (dùng auth file có sẵn từ run trước)
npx playwright test --project=chromium --no-deps

# Chỉ refresh auth (chạy lại setup mà không chạy main tests)
npx playwright test --project=setup

Flag --no-deps bỏ qua dependency graph — main project chạy ngay dù setup chưa chạy. Hữu ích khi auth file còn hợp lệ (ví dụ chạy trong ngày dev, token chưa expire) và bạn chỉ muốn chạy lại test spec cụ thể:

# Chạy một file test cụ thể, skip setup
npx playwright test tests/checkout.spec.ts --project=chromium --no-deps

# Chạy test có tag @smoke, skip setup
npx playwright test --grep @smoke --project=chromium --no-deps

Chú ý: --no-deps không disable storageState trong config — main project vẫn load user.json như bình thường. Flag này chỉ bỏ qua bước chờ setup project complete. Nếu user.json không tồn tại hoặc invalid, tests sẽ fail với lỗi auth, không phải lỗi file not found.

10

Setup Fail Behavior

Khi setup project fail (bất kỳ test trong *.setup.ts fail), các main project depend vào nó không chạy — status là skipped (cụ thể là expected-skipped trong API):

  1 failed
    [setup] › tests/auth.setup.ts:8:1 › authenticate

  3 skipped
    [chromium] ↳ skipped due to failed dependency
    [firefox]  ↳ skipped due to failed dependency
    [webkit]   ↳ skipped due to failed dependency

Ưu điểm so với globalSetup: reporter hiển thị rõ ràng "skipped due to failed dependency" thay vì main tests fail với lỗi auth không rõ nguồn gốc. Trace và screenshot của setup test vẫn được lưu — debug ngay tại bước login.

Nếu setup có retries, test runner retry setup trước khi mark fail. Ví dụ nếu config retries: 1, setup test được thử 2 lần. Chỉ khi cả 2 lần đều fail mới block main projects.

11

Per-Worker Auth (Advanced Preview)

Bài này đề cập qua để bạn biết pattern tồn tại. Deep dive sẽ ở bài 522–524.

Setup project pattern dùng 1 auth file chia sẻ cho tất cả worker. Điều này hoạt động tốt khi app không có conflict giữa concurrent session. Nhưng với app có single-session policy (mỗi account chỉ được login từ 1 nơi đồng thời), nhiều worker cùng dùng 1 storageState dẫn đến session kick lẫn nhau.

Per-worker auth giải quyết bằng cách mỗi worker login vào một account riêng:

// Dùng workerIndex để tạo auth file riêng mỗi worker
const authFile = `playwright/.auth/user-${workerInfo.parallelIndex}.json`;

Pattern đầy đủ với test.beforeAllworkerInfo.parallelIndex được trình bày ở bài 522–524 cùng với chiến lược quản lý account pool.

12

Best Practices

Đặt tên file setup nhất quán

Convention phổ biến: *.setup.ts (ví dụ auth.setup.ts, admin.setup.ts). Regex /.*\.setup\.ts/ dùng trong testMatch khớp chính xác pattern này. Đặt tên khác như setup-auth.ts hoặc global.setup.ts cần điều chỉnh regex tương ứng.

Thêm assertion trong setup

Luôn assert trạng thái sau login trước khi gọi storageState:

// Không nên — save blind, không biết login thành công không
await page.context().storageState({ path: authFile });

// Nên — assert trước
await expect(page).toHaveURL('/dashboard');
await expect(page.getByTestId('user-avatar')).toBeVisible();
await page.context().storageState({ path: authFile });

Gitignore thư mục auth

File *.json trong playwright/.auth/ chứa session token và cookie — không commit vào repo:

# .gitignore
playwright/.auth/

Tuy nhiên cần đảm bảo thư mục tồn tại. Thêm playwright/.auth/.gitkeep rồi gitignore chỉ file JSON:

# .gitignore
playwright/.auth/*.json

Tách setup khỏi testDir nếu cần

Nếu testDir: './tests' và setup file nằm trong tests/, setup project sẽ nhặt đúng. Nhưng nếu bạn muốn setup file nằm ngoài tests/ (ví dụ ./setup/auth.setup.ts), cần set testDir riêng trên setup project:

{
  name: 'setup',
  testDir: './setup',
  testMatch: /.*\.setup\.ts/,
}
13

Limitation

  • Setup chạy lại mỗi invocation — không cache auth file giữa các lần chạy. Mỗi npx playwright test đều login lại. Nếu login flow chậm, tổng thời gian tăng. Giải pháp thủ công: dùng --no-deps khi auth cũ còn hợp lệ.
  • Token expire mid-run — nếu suite chạy lâu và session timeout ngắn, một số test cuối sẽ fail với lỗi unauthenticated dù setup đã thành công. Không có cơ chế tự refresh token giữa chừng trong setup project pattern cơ bản.
  • State chỉ qua file — setup và main projects không share JavaScript heap. Mọi thứ phải đi qua file trên disk. Nếu cần truyền data phức tạp hơn auth state (ví dụ ID của entity vừa tạo), phải ghi ra file JSON riêng và đọc lại trong test.
  • Dependency chỉ theo chiều một chiều — main project depend vào setup project, không thể có dependency vòng tròn. Nếu hai setup project depend vào nhau, config sẽ báo lỗi circular dependency.
14

4 Pitfall

Pitfall 1 — Setup quên gọi storageState

// Sai — login xong nhưng không save state
setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.waitForURL('/dashboard');
  // Thiếu: await page.context().storageState({ path: authFile });
});

Kết quả: setup test pass (không có assertion fail), nhưng user.json không được cập nhật (hoặc không tồn tại). Main tests load file cũ hoặc file trống — mọi test fail với lỗi auth không rõ nguồn gốc, khó debug vì setup report xanh hoàn toàn.

Pitfall 2 — testMatch sai nên setup không chạy

// Config setup project
{
  name: 'setup',
  testMatch: /.*\.setup\.ts/,  // Regex này
}

// File setup đặt tên không khớp
// tests/setupAuth.ts     ← không khớp (thiếu dấu chấm trước setup)
// tests/auth-setup.ts    ← không khớp (setup ở giữa không đúng pattern)
// tests/auth.setup.ts    ← khớp đúng

Khi testMatch không khớp, setup project chạy 0 test — exit code 0 (không có test nào fail). Dependencies được coi là satisfied, main tests chạy ngay với auth file cũ hoặc không tồn tại. Luôn verify bằng npx playwright test --list --project=setup để kiểm tra setup project thấy đúng file.

Pitfall 3 — Main project quên dependencies

// Sai — không có dependencies
{
  name: 'chromium',
  use: {
    ...devices['Desktop Chrome'],
    storageState: 'playwright/.auth/user.json',
  },
  // Thiếu: dependencies: ['setup']
}

Test runner không đảm bảo thứ tự — setup và chromium project có thể chạy song song. Nếu chromium worker khởi động trước setup hoàn thành, nó đọc user.json chưa được ghi (stale hoặc không tồn tại). Kết quả: race condition — đôi khi pass, đôi khi fail tùy tốc độ worker. Race condition khó debug hơn fail nhất quán rất nhiều.

Pitfall 4 — Token expire giữa chừng

Setup chạy đầu suite và tạo auth file thành công. Suite có 200 test, chạy mất 45 phút. Session timeout của app là 30 phút. Test thứ 150 trở đi gặp redirect về trang login — fail với lỗi expect(page).toHaveURL('/checkout') nhưng thực tế page đang ở /login.

Dấu hiệu nhận biết: test fail tập trung ở phần cuối report, lỗi đều liên quan đến URL không expected. Không liên quan đến nội dung test logic.

Giải pháp tùy context: kéo dài session timeout trong môi trường test, hoặc dùng API để lấy token không expire, hoặc implement per-worker auth với token có TTL dài hơn thời gian chạy suite.

15

Quiz

Câu 1. Config sau có đúng không? Nếu sai, vấn đề là gì?

projects: [
  {
    name: 'setup',
    testMatch: /.*\.setup\.ts/,
  },
  {
    name: 'chromium',
    use: {
      ...devices['Desktop Chrome'],
      storageState: 'playwright/.auth/user.json',
    },
  },
],
Đáp án

Sai. Project 'chromium' thiếu dependencies: ['setup']. Không có dependency, test runner không đảm bảo setup hoàn thành trước chromium — race condition. Dù setup chạy trước về mặt thực tế phần lớn thời gian, đây không phải đảm bảo chính thức và sẽ fail ngẫu nhiên.

Câu 2. Setup project chạy và report xanh hoàn toàn, nhưng main tests fail với lỗi redirect về trang login. File playwright/.auth/user.json tồn tại nhưng chứa {"cookies":[],"origins":[]}. Nguyên nhân?

Đáp án

Setup test quên gọi await page.context().storageState({ path: authFile }) — hoặc gọi trước khi login hoàn thành. File JSON được ghi (hoặc đã tồn tại từ run trước) nhưng nội dung trống: cookies và origins đều empty. Main project load file này → context không có session → redirect về login. Setup pass vì không có assertion fail trong setup test.

Câu 3. Bạn có suite dùng setup project, chạy đầy đủ mất 40 phút. Trong giờ dev, bạn muốn sửa một test spec và chạy lại nhanh mà không login lại. Lệnh nào dùng?

Đáp án

npx playwright test tests/checkout.spec.ts --project=chromium --no-deps. Flag --no-deps bỏ qua bước chờ setup, chromium project chạy ngay và load auth file hiện có. Phù hợp khi auth file còn hợp lệ trong ngày.

Câu 4. Config multi-role sau có vấn đề gì?

projects: [
  { name: 'setup-admin', testMatch: /admin-setup\.ts/ },
  { name: 'setup-user', testMatch: /user-setup\.ts/ },
  {
    name: 'admin-tests',
    dependencies: ['setup-admin', 'setup-user'],
    use: { storageState: 'playwright/.auth/admin.json' },
    testMatch: /admin\..*\.spec\.ts/,
  },
]
Đáp án

admin-tests depend vào cả setup-admin lẫn setup-user, nhưng chỉ dùng admin.json. Dependency vào setup-user thừa — admin tests không cần user auth sẵn sàng. Không phải lỗi crash nhưng gây chậm không cần thiết: admin tests phải chờ user setup hoàn thành dù không dùng kết quả. Sửa lại: dependencies: ['setup-admin'].

Câu 5. Tại sao setup project chạy trên Chromium có thể tạo auth file dùng được cho main project Firefox và WebKit?

Đáp án

Auth file (storageState) chứa cookies và localStorage values — đây là dữ liệu server trả về và lưu trong browser storage, không phụ thuộc browser engine. Cookie session_id=abc123 set bởi server là giống nhau dù tạo ra từ Chromium hay Firefox. Khi Firefox context load storageState, nó inject đúng cookies và localStorage vào context mới — server nhận request với đầy đủ auth headers và xác thực thành công. Ngoại lệ: login flow dùng WebAuthn hoặc browser-specific API cần setup riêng per browser.

16

Bài Tiếp Theo