Danh sách bài viết

Bài 22: Fixture auto: true — Tự Động Chạy Mỗi Test

Fixture thường chỉ chạy khi test destructure tên fixture trong tham số. auto: true thay đổi hành vi đó: fixture chạy tự động cho mọi test trong scope — không cần test "yêu cầu". Bài này cover cú pháp, type void, use case logging / telemetry / screenshot-on-failure / pre-flight check / worker stats, so sánh với beforeEach hook, combine với non-auto fixture, limitations và 4 pitfall hay gặp.

27/05/2026
14 phút đọc
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ẽ:

  • Hiểu sự khác biệt giữa fixture thường và fixture có auto: true.
  • Khai báo đúng cú pháp auto: true và type void cho fixture không trả value.
  • Triển khai các use case thực tế: logging, telemetry, screenshot-on-failure, pre-flight check.
  • Dùng auto: true kết hợp với scope: 'worker' cho worker-level monitoring.
  • Biết khi nào dùng auto fixture thay vì beforeEach hook.
  • Tránh 4 pitfall hay gặp khi dùng auto fixture.
2

Vấn Đề auto: true Giải Quyết

Fixture thường trong Playwright hoạt động theo cơ chế lazy: fixture chỉ chạy khi test khai báo nó trong destructuring. Điều này tốt cho resource nặng, nhưng tạo ra vấn đề cho các fixture chỉ có side-effect — fixture mà test không cần nhận value, chỉ cần fixture "chạy".

Ví dụ: bạn muốn log tên test, thời gian chạy, và gửi metric đến hệ thống monitoring. Mỗi test cần cái đó — nhưng không có test nào cần destructure một giá trị từ fixture logging. Nếu dùng fixture thường:

// Fixture thường — test PHẢI destructure để chạy
test('checkout flow', async ({ page, logger }) => {
  // logger phải có ở đây dù test không dùng giá trị logger
  // Nếu quên → fixture không chạy
  await page.goto('/checkout');
});

Yêu cầu test phải khai báo logger dù không dùng value là noise, và nếu team quên — fixture không chạy. auto: true loại bỏ yêu cầu đó:

// Auto fixture — chạy cho mọi test, test không cần khai báo
test('checkout flow', async ({ page }) => {
  // logger tự chạy — test không cần biết
  await page.goto('/checkout');
});
3

Cú Pháp

Fixture có auto: true phải được khai báo dưới dạng tuple hai phần tử — thay vì function trực tiếp:

import { test as base } from '@playwright/test';

export const test = base.extend<{ logger: void }>({
  logger: [
    async ({}, use, testInfo) => {
      // Setup: chạy trước test body
      console.log(`Starting: ${testInfo.title}`);

      await use(undefined); // type void → truyền undefined

      // Teardown: chạy sau test body
      console.log(`Finished: ${testInfo.title} — status: ${testInfo.status}`);
    },
    { auto: true }, // <-- flag quan trọng
  ],
});

Cú pháp tuple [fixtureFunction, options] là cách duy nhất để truyền options cho fixture. Ngoài auto: true, cùng cú pháp này được dùng để truyền scope, timeout, box, và title cho fixture.

Tham số thứ ba của fixture function — testInfo — là object TestInfo chứa metadata của test đang chạy: title, status, expectedStatus, duration, retry, v.v. Auto fixture dùng testInfo nhiều vì đây là nơi "quan sát" hành vi test mà không can thiệp vào logic.

4

Type void Và Lý Do Dùng Nó

Fixture có auto: true thường không truyền value hữu ích cho test — mục đích là side-effect. Khi type fixture là void, use(undefined) là cách gọi đúng:

// Type fixture = void
export const test = base.extend<{ logger: void }>({
  logger: [
    async ({}, use) => {
      // setup...
      await use(undefined); // hoặc gọn hơn: await use()
      // teardown...
    },
    { auto: true },
  ],
});

Một số codebase dùng null thay void — cả hai đều hoạt động vì test không bao giờ dùng giá trị đó. Convention phổ biến là void vì nó diễn đạt rõ hơn ý nghĩa "không có giá trị trả về".

Khi gọi use() không có argument thay vì use(undefined), TypeScript có thể báo lỗi tùy cấu hình strictNullChecks. Để an toàn, dùng await use(undefined) tường minh.

5

Use Case: Logging

Use case đơn giản nhất: ghi log tên test và kết quả. Hữu ích khi CI không có Playwright HTML report, hoặc khi cần pipe test output vào log aggregation:

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

export const test = base.extend<{ testLogger: void }>({
  testLogger: [
    async ({}, use, testInfo) => {
      const startedAt = Date.now();
      console.log(`[TEST START] ${testInfo.title}`);

      await use(undefined);

      const duration = Date.now() - startedAt;
      const statusLabel = testInfo.status === testInfo.expectedStatus ? 'PASS' : 'FAIL';
      console.log(`[TEST END] ${testInfo.title} — ${statusLabel} — ${duration}ms`);
    },
    { auto: true },
  ],
});

export { expect } from '@playwright/test';

Test file không cần biết về testLogger:

// tests/checkout.spec.ts
import { test, expect } from '../fixtures';

test('add to cart', async ({ page }) => {
  await page.goto('/products');
  await page.getByRole('button', { name: 'Add to cart' }).first().click();
  await expect(page.getByRole('status')).toContainText('1 item');
});

test('checkout requires login', async ({ page }) => {
  await page.goto('/checkout');
  await expect(page).toHaveURL('/login');
});

Output console:

[TEST START] add to cart
[TEST END] add to cart — PASS — 1240ms
[TEST START] checkout requires login
[TEST END] checkout requires login — PASS — 380ms

Lưu ý: testInfo.status chỉ có giá trị cuối cùng sau khi test body hoàn thành. Đọc nó trong phần teardown (sau await use()) mới có kết quả chính xác.

6

Use Case: Telemetry

Gửi metric đến hệ thống monitoring (Datadog, Prometheus, custom backend) sau mỗi test. Auto fixture phù hợp vì logic này không thuộc test logic — test không cần biết về telemetry:

export const test = base.extend<{ telemetry: void }>({
  telemetry: [
    async ({}, use, testInfo) => {
      const startedAt = Date.now();

      await use(undefined);

      // Gửi metric sau khi test hoàn thành
      const metric = {
        testTitle: testInfo.title,
        file: testInfo.file,
        project: testInfo.project.name,
        status: testInfo.status,
        duration: Date.now() - startedAt,
        retryIndex: testInfo.retry,
      };

      // Fire-and-forget — không await để không làm chậm teardown
      fetch('https://metrics.internal/playwright', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(metric),
      }).catch(() => {
        // Suppress lỗi network — không để monitoring làm fail test
      });
    },
    { auto: true },
  ],
});

Một điểm quan trọng: gọi fetch() không await để không block teardown. Nếu endpoint monitoring chậm hoặc down, test không bị ảnh hưởng. Tuy nhiên, nếu cần đảm bảo metric được gửi thành công trước khi worker kết thúc, cân nhắc dùng worker-scope auto fixture (bài 9 bên dưới).

7

Use Case: Screenshot On Failure

Pattern phổ biến nhất cho auto fixture: tự chụp screenshot khi test fail. Playwright có cơ chế screenshot: 'only-on-failure' trong config, nhưng auto fixture cho phép kiểm soát chi tiết hơn — ví dụ attach vào TestInfo, tùy chỉnh tên file, hoặc xử lý logic phức tạp hơn:

export const test = base.extend<{
  autoScreenshot: void;
}>({
  autoScreenshot: [
    async ({ page }, use, testInfo) => {
      await use(undefined);

      // Chỉ chụp khi test fail (status khác expectedStatus)
      if (testInfo.status !== testInfo.expectedStatus) {
        const filename = testInfo.title.replace(/\s+/g, '-').replace(/[^a-z0-9\-]/gi, '');
        const screenshotPath = `screenshots/${filename}-${testInfo.retry}.png`;

        await page.screenshot({
          path: screenshotPath,
          fullPage: true,
        });

        // Attach vào TestInfo để hiển thị trong HTML report
        await testInfo.attach('failure-screenshot', {
          path: screenshotPath,
          contentType: 'image/png',
        });
      }
    },
    { auto: true },
  ],
});

Fixture này depend vào page (built-in) — không có gì đặc biệt khi auto fixture depend vào fixture khác. Playwright resolve dependency graph bình thường: page được khởi tạo trước, sau đó autoScreenshot chạy (và nhận page đã có).

Điều kiện testInfo.status !== testInfo.expectedStatus cũng bắt trường hợp test được đánh dấu test.fail() nhưng lại pass — đó cũng là trạng thái bất thường cần điều tra.

8

Use Case: Pre-flight Check

Kiểm tra health của app trước mỗi test. Hữu ích trong môi trường staging không ổn định — nếu app đang down, test fail ngay với error message rõ ràng thay vì fail ngẫu nhiên giữa chừng:

export const test = base.extend<{ healthCheck: void }>({
  healthCheck: [
    async ({ request }, use, testInfo) => {
      // Kiểm tra health endpoint trước test
      const response = await request.get('/api/health');

      if (!response.ok()) {
        // Báo lỗi rõ ràng — test sẽ fail ở đây với message có ý nghĩa
        throw new Error(
          `App health check failed (HTTP ${response.status()}) before test: "${testInfo.title}"`
        );
      }

      await use(undefined);
    },
    { auto: true },
  ],
});

Khi fixture setup throw exception, Playwright đánh dấu test là failed và bỏ qua test body. Error message từ throw hiển thị trong report — tốt hơn nhiều so với test fail vì timeout hay element not found.

Cân nhắc: pre-flight check gọi network trước mỗi test — nếu test suite có 200 test thì sẽ có 200 HTTP call thêm. Cân bằng giữa độ chắc chắn và thời gian chạy. Một cách khác: dùng worker-scope auto fixture để check một lần mỗi worker thay vì mỗi test.

9

Worker Auto Fixture

auto: true có thể kết hợp với scope: 'worker' để tạo fixture chạy một lần khi worker khởi động và một lần khi worker kết thúc — thay vì mỗi test. Khai báo trong WorkerFixtures (tham số thứ hai của generic):

import { test as base } from '@playwright/test';

export const test = base.extend<
  {},                        // TestFixtures — không có test-scope fixture mới
  { workerStats: void }      // WorkerFixtures — worker-scope fixture
>({
  workerStats: [
    async ({}, use, workerInfo) => {
      console.log(`Worker ${workerInfo.workerIndex} starting`);
      const startTime = Date.now();

      await use(undefined);

      const duration = Date.now() - startTime;
      console.log(`Worker ${workerInfo.workerIndex} finished — total duration: ${duration}ms`);
    },
    { scope: 'worker', auto: true },
  ],
});

Fixture function nhận workerInfo (thay vì testInfo) làm tham số thứ ba khi scope: 'worker'. workerInfo có các field: workerIndex (index của worker trong run này), parallelIndex (dùng cho parallel execution), và project.

Worker auto fixture cho health check hiệu quả hơn

Thay vì check health mỗi test (bài 8), check một lần mỗi worker:

export const test = base.extend<
  {},
  { workerHealthCheck: void }
>({
  workerHealthCheck: [
    async ({ browser }, use, workerInfo) => {
      // Tạo context tạm để check health — không dùng context của test
      const ctx = await browser.newContext();
      const page = await ctx.newPage();
      const response = await page.request.get('/api/health');
      await ctx.close();

      if (!response.ok()) {
        throw new Error(
          `Worker ${workerInfo.workerIndex}: App health check failed (HTTP ${response.status()})`
        );
      }

      await use(undefined);
    },
    { scope: 'worker', auto: true },
  ],
});

Worker-scope auto fixture giảm số lần gọi health check từ N (số test) xuống M (số worker) — thường M nhỏ hơn N đáng kể.

10

So Sánh Với beforeEach Hook

Auto fixture và beforeEach hook đều chạy trước test, nhưng có các điểm khác biệt quan trọng:

Auto fixture beforeEach hook
Scope áp dụng Mọi test dùng test đã extend — cross-file Chỉ test trong cùng describe hoặc file
Teardown Code sau await use() — chạy dù pass hay fail Cần afterEach riêng
Reusable Có — import từ fixtures file Không — phải lặp lại hoặc wrap vào helper function
Dependencies Khai báo tường minh — Playwright resolve tự động Thủ công, phải tự quản lý
Worker scope Có — scope: 'worker', auto: true Không có tương đương trực tiếp (beforeAll gần nhất)
Testinfo access Có — tham số thứ ba của fixture function Có — tham số của callback
Phù hợp cho Project-wide concern (logging, monitoring, global setup) Logic chỉ dùng trong file hoặc describe block cụ thể

Heuristic: nếu logic cần chạy cho tất cả test trong project (hoặc một tập lớn test cross-file), dùng auto fixture. Nếu chỉ cần cho test trong một describe block của một file cụ thể, beforeEach đơn giản hơn và dễ đọc hơn.

11

Combine Với Non-auto Fixture

Auto fixture và fixture thường có thể sống trong cùng một test.extend(). Không có xung đột — mỗi fixture hoạt động độc lập theo cơ chế của nó:

import { test as base, type Page } from '@playwright/test';

export const test = base.extend<{
  testLogger: void;     // auto — chạy mọi test
  authedPage: Page;     // non-auto — chỉ chạy khi test request
}>({
  // Auto fixture — chạy cho mọi test
  testLogger: [
    async ({}, use, testInfo) => {
      console.log(`[START] ${testInfo.title}`);
      await use(undefined);
      console.log(`[END] ${testInfo.title} — ${testInfo.status}`);
    },
    { auto: true },
  ],

  // Non-auto fixture — chỉ chạy khi test destructure authedPage
  authedPage: async ({ page }, use) => {
    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');
    await use(page);
  },
});

export { expect } from '@playwright/test';

Kết quả: testLogger chạy cho tất cả test. authedPage chỉ chạy khi test khai báo nó:

// testLogger chạy, authedPage KHÔNG chạy
test('public page loads', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle('Home');
});

// testLogger chạy, authedPage CŨNG chạy
test('dashboard visible', async ({ authedPage }) => {
  await expect(authedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
12

Limitations

Auto fixture có những giới hạn cần biết trước khi áp dụng:

1. Setup chậm ảnh hưởng mọi test

Fixture thường chỉ làm chậm test nào request nó — lazy. Auto fixture làm chậm mọi test. Nếu setup của auto fixture mất 500ms (vd HTTP call health check), toàn bộ suite bị cộng 500ms × số test.

Đây là lý do auto fixture cần giữ lightweight. Nếu setup nặng cần thiết, cân nhắc worker-scope auto fixture (chạy một lần mỗi worker thay vì mỗi test).

2. Teardown throw exception — mask lỗi gốc

Nếu teardown của auto fixture throw exception (vd gửi metric thất bại với error), Playwright có thể báo lỗi fixture thay vì lỗi gốc từ test. Debug trở nên khó hơn vì error message không liên quan đến test logic. Giải pháp: wrap teardown trong try/catch và suppress lỗi không quan trọng:

testLogger: [
  async ({}, use, testInfo) => {
    await use(undefined);

    // Wrap teardown — không để fixture lỗi mask test lỗi
    try {
      await sendMetric(testInfo);
    } catch {
      // Log nhưng không rethrow
      console.error('Failed to send metric — continuing');
    }
  },
  { auto: true },
],

3. Khó debug khi fixture fail

Khi auto fixture fail trong setup (trước use()), test bị đánh dấu failed nhưng không có stack trace từ test body. Với fixture thường, ít nhất bạn biết test đang dùng fixture nào (qua destructuring). Auto fixture fail "ngầm" hơn — cần đọc stack trace kỹ để biết là fixture hay test logic gây ra.

4. Worker auto fixture mutate shared state — bug subtle

Worker-scope auto fixture chia sẻ state giữa các test trong cùng worker. Nếu fixture lưu data vào object được mutate bởi từng test, test có thể ảnh hưởng lẫn nhau. Tránh lưu state mutable trong worker auto fixture — chỉ dùng cho side-effect thuần (logging, monitoring).

13

Common Pitfalls

1. Auto fixture nặng — toàn bộ suite chậm

// SAI — setup đắt chạy mọi test
heavySetup: [
  async ({}, use) => {
    await seedDatabase(1000); // 5 giây mỗi test → 5s × 200 test = 1000 giây thêm
    await use(undefined);
    await clearDatabase();
  },
  { auto: true },
],

// TỐT HƠN — worker scope nếu setup có thể chia sẻ
heavySetup: [
  async ({}, use) => {
    await seedDatabase(1000); // chỉ chạy khi worker start
    await use(undefined);
    await clearDatabase();
  },
  { scope: 'worker', auto: true },
],

// HOẶC — không dùng auto, để test chọn khi cần
heavySetup: async ({}, use) => {
  await seedDatabase(1000);
  await use(undefined);
  await clearDatabase();
},

2. Quên auto: true — fixture không chạy khi test không destructure

// SAI — không có auto: true → fixture chỉ chạy khi test destructure
testLogger: async ({}, use, testInfo) => {
  console.log(`Starting: ${testInfo.title}`);
  await use(undefined);
  console.log(`Done: ${testInfo.title}`);
},

// ĐÚNG — tuple với options object
testLogger: [
  async ({}, use, testInfo) => {
    console.log(`Starting: ${testInfo.title}`);
    await use(undefined);
    console.log(`Done: ${testInfo.title}`);
  },
  { auto: true }, // bắt buộc
],

Đây là lỗi thường gặp nhất với auto fixture. Biểu hiện: log không xuất hiện cho các test không khai báo fixture trong destructuring — nhưng xuất hiện bình thường cho test có khai báo.

3. Teardown throw exception — mask lỗi gốc của test

// SAI — teardown không được bảo vệ
metricFixture: [
  async ({}, use, testInfo) => {
    await use(undefined);
    await sendToRemoteService(testInfo); // nếu throw → báo lỗi fixture, không lỗi test
  },
  { auto: true },
],

// ĐÚNG — bọc teardown trong try/catch
metricFixture: [
  async ({}, use, testInfo) => {
    await use(undefined);
    try {
      await sendToRemoteService(testInfo);
    } catch (err) {
      console.error('Metric send failed:', err);
      // Không rethrow — để lỗi gốc của test được hiển thị
    }
  },
  { auto: true },
],

4. Worker auto fixture mutate state — test ảnh hưởng lẫn nhau

// SAI — array được push bởi từng test trong worker
const testResults: string[] = [];

const test = base.extend<{}, { collector: void }>({
  collector: [
    async ({}, use, workerInfo) => {
      await use(undefined);
      testResults.push(`worker-${workerInfo.workerIndex}-done`); // BUG: shared mutation
    },
    { scope: 'worker', auto: true },
  ],
});

// ĐÚNG — nếu cần collect, dùng worker-scoped non-auto fixture
// và để mỗi test tự report qua testInfo.attach() hoặc annotation
14

Tổng Kết

  • auto: true khiến fixture chạy tự động cho mọi test trong scope — không cần test destructure tên fixture.
  • Cú pháp là tuple: [fixtureFunction, { auto: true }]. Không thể truyền auto: true với cú pháp function thông thường.
  • Type convention: void cho fixture không trả value. Gọi await use(undefined).
  • Tham số thứ ba của fixture function là testInfo (test-scope) hoặc workerInfo (worker-scope).
  • Use case chính: logging, telemetry, screenshot-on-failure, pre-flight check — những thứ chạy "ngầm" mà test không cần biết.
  • Kết hợp scope: 'worker'auto: true để chạy fixture một lần mỗi worker thay vì mỗi test.
  • Auto fixture và non-auto fixture cùng tồn tại trong test.extend() — không xung đột.
  • Giữ auto fixture lightweight. Setup nặng trong auto fixture ảnh hưởng toàn bộ suite.
  • Wrap teardown trong try/catch để tránh lỗi fixture mask lỗi gốc của test.
15

Bài Tập Củng Cố

Câu 1

Đoạn code dưới đây định nghĩa logging fixture nhưng log không xuất hiện khi test không khai báo logger trong destructuring. Lỗi ở đâu?

export const test = base.extend<{ logger: void }>({
  logger: async ({}, use, testInfo) => {
    console.log(`[START] ${testInfo.title}`);
    await use(undefined);
    console.log(`[END] ${testInfo.title}`);
  },
});
Đáp án

Thiếu auto: true. Fixture được định nghĩa dưới dạng function thông thường — không phải tuple. Playwright chỉ chạy nó khi test destructure logger. Sửa lại:

logger: [
  async ({}, use, testInfo) => {
    console.log(`[START] ${testInfo.title}`);
    await use(undefined);
    console.log(`[END] ${testInfo.title}`);
  },
  { auto: true },
],

Câu 2

Viết một auto fixture tên retryLogger chỉ log khi test đang được retry (không phải lần chạy đầu tiên). Fixture không cần dependency nào.

Đáp án
retryLogger: [
  async ({}, use, testInfo) => {
    if (testInfo.retry > 0) {
      console.log(`[RETRY #${testInfo.retry}] ${testInfo.title}`);
    }
    await use(undefined);
  },
  { auto: true },
],

testInfo.retry là số lần retry hiện tại: 0 = lần chạy gốc, 1 = retry thứ nhất, v.v.

Câu 3

Fixture autoScreenshot depend vào page (built-in). Nếu test không dùng browser (ví dụ test thuần API không cần page), điều gì xảy ra khi autoScreenshot chạy? Có vấn đề gì không?

Đáp án

Khi autoScreenshot depend vào page, Playwright phải khởi tạo page (và context, browser theo chain) dù test không cần browser. Điều này:

  • Làm chậm test API vì phải launch browser không cần thiết.
  • Tốn resource — context và page được tạo và destroy mà không dùng.

Giải pháp: kiểm tra xem page có được dùng trong test hay không trước khi chụp. Một cách thực tế là không depend vào page trong fixture definition — thay vào đó inject qua dependency thủ công hoặc dùng cơ chế khác (vd testInfo.attachments từ Playwright built-in).

Hoặc đơn giản hơn: chỉ dùng auto screenshot fixture cho test file biết mình cần browser, không áp dụng global cho toàn project.

Câu 4

Bạn muốn đo tổng thời gian của mỗi worker (không phải từng test). Viết worker-scope auto fixture workerTimer log workerIndex và tổng thời gian worker chạy.

Đáp án
export const test = base.extend<
  {},
  { workerTimer: void }
>({
  workerTimer: [
    async ({}, use, workerInfo) => {
      const start = Date.now();
      console.log(`Worker ${workerInfo.workerIndex} started`);

      await use(undefined);

      const duration = Date.now() - start;
      console.log(`Worker ${workerInfo.workerIndex} finished — ${duration}ms`);
    },
    { scope: 'worker', auto: true },
  ],
});

Type khai báo: workerTimer vào WorkerFixtures (tham số thứ hai). Nếu để trong TestFixtures, TypeScript sẽ không cho phép dùng scope: 'worker'.

Câu 5

Một team đặt câu hỏi: "Chúng tôi có một beforeEach hook trong file auth.spec.ts để reset mock auth state. Khi nào thì nên convert sang auto fixture?"

Đáp án

Giữ nguyên beforeEach nếu:

  • Logic này chỉ cần trong auth.spec.ts — không có file nào khác cần nó.
  • Logic đơn giản, không cần teardown phức tạp.

Convert sang auto fixture khi:

  • Logic xuất hiện ở nhiều file spec — copy-paste báo hiệu cần refactor.
  • Cần teardown (reset state sau test) — fixture đảm bảo teardown chạy dù test fail.
  • Logic phụ thuộc vào fixture khác và muốn Playwright quản lý dependency thay vì tự gọi thủ công.

Trong trường hợp này, nếu chỉ một file cần reset mock auth state thì beforeEach là lựa chọn đúng. Nếu nhiều file cần — chuyển sang auto fixture.

16

Bài Tiếp Theo

Bài 23 tiếp tục A.3 với option: true — cơ chế parametrize fixture qua project config.

Bài 23: Fixture option: true — Parametrize Qua Project Config