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 sauuse()đóng vai trò teardown.page,context,browser,requestđều là fixture có sẵn. - test.extend() tạo ra một object
testmới có thêm fixture tự viết — mọi spec file importtesttừ 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ồibase.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ộcpagethì 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ằngtest.use()— ví dụ fixturethememặ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
testnày đều cóloginPagesẵ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.tsduy nhất, export cảtestlẫnexpect— mọi spec import từ đây, tránh tình trạng file này dùngtestgốc file kia dùngtestmở 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ướcuse, mọi teardown viết sauuse.- Worker fixture không được phụ thuộc test fixture —
page/contextlà 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: truechạ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ộttestđã extend, miễn không trùng tên.
