Wiki

← Tất cả bài wiki

Custom Fixtures (Mở rộng test bằng test.extend)

automationplaywright

Fixture là cơ chế dependency injection của Playwright Test — mọi thứ test dùng (page, context, data, Page Object) đều được cung cấp qua fixture. Custom fixture bằng test.extend() giúp đóng gói logic setup/teardown, inject POM và auth state theo kiểu composition thay vì lặp beforeEach, và là nền tảng để scale test suite lớn.

Khái niệm & Bản chất

  • Fixture là một hàm setup chạy quanh test: Playwright gọi nó, nó "phát" giá trị cho test dùng qua await use(value), rồi phần code sau use() đóng vai trò teardown. page, context, browser, request đều là fixture có sẵn.
  • test.extend() tạo ra một object test mới có thêm fixture tự viết — mọi spec file import test từ file fixture sẽ tự động nhận được setup/teardown đó.
  • Hai scope: test-scoped (mặc định — fixture chạy lại cho TỪNG test, cô lập) và worker-scoped ({ scope: 'worker' } — chạy một lần mỗi worker process, dùng cho tài nguyên đắt như account, connection).
  • Auto fixture ({ auto: true }): chạy tự động cho mọi test kể cả khi test không khai báo nó — hợp với cross-cutting concern như collect console error, bật mock API.
  • Fixture là lazy: chỉ fixture nào thực sự được test dùng (trực tiếp hoặc qua dependency) mới được khởi tạo.

Cách hoạt động & API chính

  • Khai báo type trước: type MyFixtures = { loginPage: LoginPage }, rồi base.extend<MyFixtures>({...}) — TypeScript sẽ gợi ý fixture trong signature của test.
  • Hàm fixture nhận ({ page, ...các fixture khác }, use, testInfo) — fixture tự động resolve theo dependency graph: khai báo phụ thuộc page thì page được tạo trước.
  • Worker fixture khai báo dạng mảng: [async ({}, use, workerInfo) => {...}, { scope: 'worker' }] — chú ý dùng tuple [hàm, options], không phải object.
  • Option fixture: { option: true } cho phép test/describe override giá trị bằng test.use() — ví dụ fixture theme mặc định 'light', describe nào muốn dark thì test.use({ theme: 'dark' }).
  • Có thể override fixture có sẵn — ví dụ định nghĩa lại page để tự gắn console listener hoặc set viewport cho mọi test.

Ví dụ & Cú pháp thực tế

  • Fixture inject Page Object — mọi test dùng test này đều có loginPage sẵn: ``ts // fixtures/my-test.ts import { test as base, expect } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; // khai báo type cho fixture tự viết type MyFixtures = { loginPage: LoginPage }; export const test = base.extend<MyFixtures>({ loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); // setup chung await use(loginPage); // test chạy tại thời điểm này // code sau use() = teardown (nếu cần) }, }); export { expect } from '@playwright/test'; // trong spec: import { test, expect } from '../fixtures/my-test'; test('đăng nhập thất bại', async ({ loginPage }) => { await loginPage.submit('sai@mail.com', 'wrong'); // ... }); ``
  • Worker-scoped fixture: login admin 1 lần mỗi worker, trả về đường dẫn storageState: ``ts import { test as base } from '@playwright/test'; type WorkerFixtures = { adminAuthFile: string }; export const test = base.extend<{}, WorkerFixtures>({ adminAuthFile: [async ({ browser }, use, workerInfo) => { const file = auth-worker-${workerInfo.workerIndex}.json; const ctx = await browser.newContext(); const page = await ctx.newPage(); // login qua API cho nhanh, rồi lưu session ra file riêng theo worker await page.request.post('/api/login', { data: { user: 'admin' } }); await ctx.storageState({ path: file }); await ctx.close(); await use(file); }, { scope: 'worker' }], }); ``
  • Auto fixture bắt mọi page error và fail test nếu có JS error: ``ts export const test = base.extend<{ collectErrors: void }>({ collectErrors: [async ({ page }, use) => { const errors: string[] = []; page.on('pageerror', (e) => errors.push(e.message)); await use(); // teardown: ném lỗi để test fail nếu trang có JS error if (errors.length) throw new Error(Page errors: ${errors.join(', ')}); }, { auto: true }], // tự chạy cho mọi test, không cần khai báo trong param }); ``

Best practices & Khi nào dùng

  • Gom fixture vào một file fixtures.ts duy nhất, export cả test lẫn expect — mọi spec import từ đây, tránh tình trạng file này dùng test gốc file kia dùng test mở rộng.
  • Ưu tiên fixture hơn beforeEach khi setup cần cleanup hoặc cần reuse: fixture có teardown built-in và lazy, còn beforeEach luôn chạy và phải tự dọn.
  • Worker fixture cho tài nguyên đắt (account login, DB connection, browser context nặng) — tránh phí thời gian tạo lại mỗi test.
  • Composition nhỏ, đơn nhiệm: một fixture một việc; test chỉ nhận đúng cái nó cần → dễ đọc dependency, dễ debug.
  • Với POM: inject POM qua fixture thay vì new LoginPage(page) lặp lại trong từng test — test ngắn gọn và một chỗ sửa setup.

Gotchas, So sánh & Lưu ý

  • use() chỉ được gọi ĐÚNG một lần trong một fixture — gọi lần hai sẽ throw lỗi. Mọi setup phải xong trước use, mọi teardown viết sau use.
  • Worker fixture không được phụ thuộc test fixture — page/context là test-scoped nên trong worker fixture phải tự browser.newContext() + newPage(). Ngược lại test fixture phụ thuộc worker fixture thì OK.
  • auto: true chạy cho MỌI test kể cả test không liên quan — đừng để auto fixture làm việc nặng (gọi API, mở browser phụ).
  • So với beforeEach/afterEach: hook thì global cho file và khó chia sẻ giữa các file; fixture thì typed, lazy, compose được và teardown không bị skip khi test timeout — ưu tiên fixture.
  • Fixture chạy trong test timeout — nếu setup fixture chậm sẽ ăn vào ngân sách thời gian của test; setup nặng nên đưa lên worker scope.
  • Hai test.extend() lồng nhau merge fixture theo chuỗi — có thể test.extend() tiếp trên một test đã extend, miễn không trùng tên.