Danh sách bài viết

Bài 97: globalSetup — Config Legacy

A.11 đào sâu setup/teardown cấp toàn run, so sánh legacy globalSetup vs setup project hiện đại. Bài này tập trung hoàn toàn vào globalSetup: cú pháp khai báo trong config, hành vi thực thi 1 lần trước mọi worker, nhận FullConfig, pattern auth bằng chromium.launch() thủ công, return teardown function, use case phù hợp, giới hạn của cách tiếp cận này, 4 pitfall và quiz.

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ẽ:

  • Khai báo globalSetup trong playwright.config.ts với require.resolve().
  • Viết file global-setup.ts nhận FullConfig và export default function.
  • Hiểu globalSetup chạy 1 lần trước mọi worker và mọi project, không chạy lại giữa các test.
  • Truy cập config.projects, config.workers, config.webServer từ trong setup.
  • Viết pattern auth: chromium.launch() thủ công → login → storageStatebrowser.close().
  • Return teardown function từ globalSetup thay vì dùng file globalTeardown riêng.
  • Phân biệt rõ khi nào dùng globalSetup so với setup project (bài 56).
  • Tránh 4 pitfall: abort toàn run, quên require.resolve(), browser leak, path sai.
2

globalSetup Là Gì?

globalSetup là một option trong playwright.config.ts cho phép chỉ định một file TypeScript/JavaScript. File đó export một async function — function này được test runner gọi đúng 1 lần trước khi bất kỳ worker nào được spawn và trước khi bất kỳ test nào chạy.

Đây là cơ chế "global hook" cấp toàn bộ test run — khác hoàn toàn với beforeAll (chạy trước từng describe block) hay beforeEach (chạy trước từng test).

Vị trí của globalSetup trong lifecycle Playwright:

  1. Parse playwright.config.ts
  2. Chạy globalSetup function (1 lần duy nhất)
  3. Spawn workers
  4. Phân phối test files cho workers
  5. Workers chạy tests
  6. Tổng hợp kết quả, tạo report
  7. Chạy globalTeardown (hoặc function return từ globalSetup)

Tính năng này có từ các phiên bản đầu của Playwright Test. Tính đến v1.31, Playwright bắt đầu recommend setup project làm cách chính thức, nhưng globalSetup vẫn được hỗ trợ và không bị deprecate.

3

Cú Pháp Khai Báo

Trong playwright.config.ts, thêm field globalSetup:

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

export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  use: {
    baseURL: 'http://localhost:3000',
  },
});

File global-setup.ts phải export default một async function nhận FullConfig:

// global-setup.ts
import { FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  // Chạy 1 lần trước mọi test
  await seedDatabase();
  await startMockServer();
}

export default globalSetup;

Lý do cần require.resolve(): globalSetup nhận string path, nhưng Playwright cần đường dẫn tuyệt đối để resolve đúng bất kể working directory khi chạy lệnh. require.resolve('./global-setup') chuyển relative path thành absolute path tại thời điểm load config.

Nếu dùng path tương đối trực tiếp như globalSetup: './global-setup', Playwright vẫn thử resolve nhưng behavior có thể không nhất quán giữa các OS và working directory khác nhau — đặc biệt trên CI.

4

Hành Vi Thực Thi

Các đặc điểm hành vi cụ thể cần nắm:

  • Chạy 1 lần duy nhất — không phụ thuộc số lượng project, số worker, hay số test file. Dù có 5 project và 8 worker, globalSetup vẫn chỉ chạy 1 lần trước tất cả.
  • Nhận FullConfig — object chứa toàn bộ resolved config sau khi Playwright đã merge defaults. Không phải raw object từ defineConfig().
  • Async function — test runner await globalSetup hoàn thành (hoặc reject) trước khi tiếp tục. Nếu throw error hoặc reject, toàn bộ run dừng ngay.
  • Chạy trong main process — không phải worker process. Điều này có nghĩa là code trong globalSetup không có fixture, không có test runner context.
  • Không hiện trong report — globalSetup không phải là test, nên không xuất hiện trong HTML report, không có trace, không có screenshot.

Ví dụ hành vi khi globalSetup fail:

// global-setup.ts
async function globalSetup(config: FullConfig) {
  throw new Error('Database connection failed');
}

export default globalSetup;
Error: Database connection failed
    at globalSetup (/project/global-setup.ts:3:9)

  0 passed, 0 failed
  Exited with code 1

Không có test nào được báo cáo — run abort ngay tại bước globalSetup. Không có report HTML được tạo ra.

5

Truy Cập FullConfig

Object config nhận được là FullConfig — interface Playwright export từ @playwright/test. Các property hữu ích nhất:

import { FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  // Danh sách projects đã được resolve
  console.log(config.projects.length);           // số project
  console.log(config.projects[0].name);          // tên project đầu tiên
  console.log(config.projects[0].use.baseURL);   // baseURL của project đầu

  // Số worker tối đa
  console.log(config.workers);                   // số từ config hoặc CPU count

  // WebServer config (nếu có)
  if (config.webServer) {
    console.log(config.webServer.url);           // URL để wait-for
    console.log(config.webServer.command);       // lệnh start server
  }

  // Các field khác
  console.log(config.testDir);                   // thư mục test
  console.log(config.timeout);                   // global timeout ms
  console.log(config.retries);                   // số retry
}

export default globalSetup;

Một use case thực tế: đọc baseURL từ config để không hardcode URL trong globalSetup:

async function globalSetup(config: FullConfig) {
  // Đọc baseURL từ project đầu tiên thay vì hardcode
  const baseURL = config.projects[0].use.baseURL ?? 'http://localhost:3000';

  // Dùng baseURL để seed data qua API
  await fetch(`${baseURL}/api/seed`, { method: 'POST' });
}

export default globalSetup;

Lưu ý: nếu config dùng use.baseURL ở cấp global (ngoài projects), nó nằm trong config.use.baseURL. Nếu mỗi project override baseURL riêng, phải đọc từ config.projects[i].use.baseURL.

6

Use Cases

DB migration / seed

Trước khi chạy suite, đảm bảo database có đúng schema và data cần thiết. Việc này cần xảy ra đúng 1 lần — không cần reset và re-seed trước mỗi test (trừ khi test thay đổi data, nhưng đó là fixture-level concern).

import { FullConfig } from '@playwright/test';
import { runMigrations, seedTestData } from './db-helpers';

async function globalSetup(config: FullConfig) {
  await runMigrations();
  await seedTestData({
    users: 10,
    products: 50,
  });
}

export default globalSetup;

Start mock server

Spin up một mock HTTP server dùng chung cho toàn suite. Server này tồn tại trong suốt run, không khởi động lại giữa các test. Cleanup trong teardown (xem bài 98 hoặc phần return teardown ở bài này).

import { FullConfig } from '@playwright/test';
import { createMockServer } from './mock-server';

async function globalSetup(config: FullConfig) {
  const server = await createMockServer({ port: 4000 });
  // Lưu reference để teardown có thể dừng server
  (globalThis as any).__mockServer = server;
}

export default globalSetup;

Auth + save storageState

Login 1 lần, lưu storageState ra file. Mọi test load file đó thay vì login lại. Pattern này được trình bày chi tiết ở bài 7.

Env validation

Kiểm tra các service cần thiết đang chạy trước khi bắt đầu test. Fail sớm với thông báo rõ ràng thay vì để từng test fail với timeout.

import { FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const baseURL = config.projects[0].use.baseURL ?? '';

  // Kiểm tra app đang chạy
  try {
    const res = await fetch(`${baseURL}/health`);
    if (!res.ok) throw new Error(`Health check failed: ${res.status}`);
  } catch (e) {
    throw new Error(`App không accessible tại ${baseURL}. Chạy dev server trước.`);
  }

  // Kiểm tra DB connection
  const dbRes = await fetch(`${baseURL}/api/db-check`);
  if (!dbRes.ok) {
    throw new Error('Database không kết nối được.');
  }
}

export default globalSetup;
7

Pattern Auth — Lưu storageState

Đây là use case phổ biến nhất của globalSetup trong các codebase cũ. Vì globalSetup không có fixture, phải khởi động browser thủ công theo Library mode:

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import * as path from 'path';
import * as fs from 'fs';

async function globalSetup(config: FullConfig) {
  const baseURL = config.projects[0].use.baseURL ?? 'http://localhost:3000';
  const authFile = path.join(__dirname, 'playwright/.auth/user.json');

  // Đảm bảo thư mục tồn tại
  fs.mkdirSync(path.dirname(authFile), { recursive: true });

  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto(`${baseURL}/login`);
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('secret');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.waitForURL(`${baseURL}/dashboard`);

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

  await browser.close();
}

export default globalSetup;

Điểm khác biệt so với setup project (bài 56):

  • Phải gọi chromium.launch() thủ công — không có fixture page hay context.
  • Phải await browser.close() trước khi function return — không có cleanup tự động.
  • Phải tạo thư mục thủ công bằng fs.mkdirSync nếu chưa tồn tại.

Trong config, khai báo storageState ở cấp global hoặc per-project để các test tự động load:

// playwright.config.ts
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  use: {
    baseURL: 'http://localhost:3000',
    storageState: 'playwright/.auth/user.json',
  },
});
8

Return Teardown Function

globalSetup có thể return một async function. Playwright sẽ gọi function đó sau khi toàn bộ test run kết thúc — tương đương với globalTeardown nhưng gọn hơn vì cùng file:

// global-setup.ts
import { FullConfig } from '@playwright/test';
import { createMockServer, MockServer } from './mock-server';

async function globalSetup(config: FullConfig) {
  const server = await createMockServer({ port: 4000 });

  // Return teardown — chạy sau khi tất cả test kết thúc
  return async () => {
    await server.close();
    console.log('Mock server stopped');
  };
}

export default globalSetup;

Ưu điểm so với globalTeardown riêng: biến server được closure capture — không cần lưu vào globalThis hay file tạm để truyền reference sang teardown file khác.

Teardown function được gọi kể cả khi test run fail một phần — đảm bảo cleanup luôn xảy ra. Nhưng nếu globalSetup bản thân throw error (abort trước khi return), teardown function không được đăng ký và không được gọi. Cần xử lý cleanup trong catch block nếu cần:

async function globalSetup(config: FullConfig) {
  const server = await createMockServer({ port: 4000 });

  try {
    await validateEnvironment(config);
  } catch (e) {
    // Cleanup ngay nếu setup fail giữa chừng
    await server.close();
    throw e;
  }

  return async () => {
    await server.close();
  };
}

export default globalSetup;
9

globalSetup Không Có Fixture

globalSetup chạy trong main process, không phải worker process. Điều này có nghĩa là toàn bộ hệ thống fixture của Playwright — page, context, browser, request, custom fixtures — đều không khả dụng.

Để làm việc với browser trong globalSetup, phải dùng Playwright Library API trực tiếp:

// Sai — fixture không tồn tại trong globalSetup
async function globalSetup(config: FullConfig) {
  // page, context, browser — đều undefined ở đây
  await page.goto('/');  // ReferenceError: page is not defined
}

// Đúng — khởi động browser thủ công
async function globalSetup(config: FullConfig) {
  const { chromium } = await import('@playwright/test');
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('/');
  // ... thực hiện actions ...

  await browser.close();  // Bắt buộc — không có tự động cleanup
}

Không chỉ browser fixture — tất cả fixture đều không dùng được, bao gồm:

  • request (APIRequestContext) — phải dùng fetch hoặc playwright.request.newContext() thủ công.
  • Custom fixtures từ test.extend() — không inject được vào globalSetup.
  • expect với auto-wait — vẫn import được nhưng không có fixture backing.

Đây là lý do chính khiến globalSetup kém linh hoạt hơn setup project — setup project là test thực sự, được chạy trong worker với đầy đủ fixture.

10

So Sánh Với Setup Project

Bài 56 đã trình bày setup project pattern chi tiết. Bảng so sánh nhanh để phân biệt hai cách tiếp cận:

Tiêu chí globalSetup (legacy) Setup project (hiện đại)
Hiển thị trong report Không Có — project thực sự
Retry khi fail Không Có — theo retries config
Trace / Screenshot Không Có đầy đủ
Fixture Không — phải launch thủ công Có — dùng page bình thường
Số lần chạy 1 lần, cấp toàn run 1 test file, có thể nhiều test trong đó
Scope Trước mọi project, mọi worker Trước project khai báo dependencies
Debug trên CI Chỉ stdout log Trace, screenshot, video
Non-browser setup Tự nhiên — không cần browser Cần test file dù setup không dùng browser

Một điểm quan trọng ở hàng "Scope": globalSetup chạy trước tất cả projects, không phân biệt project nào. Setup project ngược lại có thể được target: chỉ project nào khai báo dependencies: ['setup'] mới chờ setup đó. Điều này linh hoạt hơn trong multi-project config.

11

Khi Nào Vẫn Dùng globalSetup

Dù setup project là approach được recommend, globalSetup vẫn phù hợp trong một số tình huống:

Legacy codebase chưa migrate

Nếu project đang có globalSetup hoạt động ổn định và không có nhu cầu debug setup bằng Trace Viewer, migration sang setup project không phải ưu tiên. Không nên migrate chỉ vì "làm mới" mà không có lý do cụ thể.

Setup thuần Node.js, không cần browser

Nếu setup chỉ cần: chạy migration script, validate env vars, kiểm tra port, start một binary — không có gì liên quan đến browser — globalSetup gọn hơn. Setup project luôn yêu cầu một test file dù setup không dùng browser:

// globalSetup phù hợp cho setup thuần Node.js
async function globalSetup(config: FullConfig) {
  // Chỉ Node.js — không cần browser
  process.env.TEST_RUN_ID = Date.now().toString();
  await runDatabaseMigrations();
  await checkRequiredEnvVars(['DB_URL', 'API_KEY', 'BASE_URL']);
}

export default globalSetup;

Env validation trước mọi thứ

globalSetup chạy trước khi bất kỳ worker nào được spawn. Nếu cần validate môi trường trước khi tốn thời gian spawn workers và phân phối test files, globalSetup là điểm can thiệp sớm nhất.

12

4 Pitfall

Pitfall 1 — globalSetup fail abort toàn run, không có report

Đây là hệ quả nguy hiểm nhất. Khi globalSetup throw error, test runner dừng ngay — không có test nào chạy, không có HTML report được tạo ra. Trên CI, điều này thường chỉ thấy qua exit code và stdout log:

Error: connect ECONNREFUSED 127.0.0.1:5432
    at globalSetup (/project/global-setup.ts:12:11)

  Error: Process completed with exit code 1.

Không có "1 failed, 50 skipped" — chỉ có crash log. Debug phải đọc raw log CI, không có Trace Viewer hỗ trợ. Đây là lý do chính Playwright recommend setup project cho auth setup có khả năng fail.

Pitfall 2 — Quên require.resolve()

// Sai — path tương đối có thể không resolve đúng trên CI
export default defineConfig({
  globalSetup: './global-setup',
});

// Đúng — absolute path, nhất quán mọi working directory
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
});

Trên local dev, path tương đối thường resolve đúng vì working directory là project root. Trên CI hoặc khi chạy từ thư mục khác, path có thể bị sai — error message thường là "Cannot find module './global-setup'" không rõ nguyên nhân.

Pitfall 3 — Quên đóng browser, gây process leak

// Sai — browser không được close nếu có exception
async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('/login');
  // Nếu dòng này throw, browser không bao giờ đóng
  await page.click('#login-button');
  await browser.close();
}

// Đúng — dùng try/finally
async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.goto('/login');
    await page.click('#login-button');
    await page.context().storageState({ path: 'playwright/.auth/user.json' });
  } finally {
    await browser.close();  // Luôn chạy dù có exception
  }
}

Browser process không đóng dẫn đến process leak — test runner có thể hang (không exit) sau khi tất cả test kết thúc, hoặc tích tụ zombie processes trên CI.

Pitfall 4 — State save path sai, test load fail

// global-setup.ts — save vào path A
await page.context().storageState({ path: 'auth/user.json' });

// playwright.config.ts — load từ path B
use: {
  storageState: 'playwright/.auth/user.json',  // Path khác!
}

Playwright không throw error nếu storageState file không tồn tại khi load — context tạo ra với state rỗng, và test fail với lỗi auth không rõ ràng. Luôn dùng cùng một path constant, tốt nhất là export từ một file shared:

// auth-paths.ts
export const AUTH_FILE = 'playwright/.auth/user.json';

// global-setup.ts
import { AUTH_FILE } from './auth-paths';
await page.context().storageState({ path: AUTH_FILE });

// playwright.config.ts
import { AUTH_FILE } from './auth-paths';
use: { storageState: AUTH_FILE }
13

Quiz

Câu 1. Config sau có vấn đề gì không?

export default defineConfig({
  globalSetup: './setup/global-setup.ts',
});
Đáp án

Có vấn đề tiềm ẩn. Path tương đối './setup/global-setup.ts' resolve dựa trên working directory tại thời điểm chạy lệnh, không phải vị trí file config. Trên CI hoặc khi chạy từ thư mục khác, path có thể fail. Đúng cách: globalSetup: require.resolve('./setup/global-setup')require.resolve() tính relative path từ vị trí file config, cho kết quả absolute path nhất quán.

Câu 2. globalSetup throw một Error. Điều gì xảy ra với các test trong suite?

Đáp án

Toàn bộ run dừng ngay lập tức — không có test nào chạy. Exit code là khác 0. Không có HTML report được tạo ra. Stdout chỉ hiện stack trace của error từ globalSetup. Đây là khác biệt quan trọng so với setup project fail: setup project fail chỉ khiến project phụ thuộc bị skip với thông báo rõ ràng trong report, các project khác vẫn chạy.

Câu 3. Đoạn code sau trong globalSetup có vấn đề gì?

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  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.context().storageState({ path: 'playwright/.auth/user.json' });
  await browser.close();
}

export default globalSetup;
Đáp án

Hai vấn đề. (1) Không có try/finally — nếu bất kỳ dòng nào từ page.goto đến storageState throw exception, browser.close() không được gọi, dẫn đến browser process leak. (2) Không có assertion trước khi save — nếu login fail (ví dụ redirect về trang lỗi thay vì dashboard), storageState vẫn lưu state chưa auth, và test sẽ fail sau với lỗi auth không rõ. Cần wrap trong try/finally và thêm await page.waitForURL('/dashboard') hoặc tương đương.

Câu 4. Bạn muốn start một mock server trước test run và đảm bảo nó được stop sau khi run kết thúc. Cách nào gọn hơn: return teardown function từ globalSetup, hay dùng file globalTeardown riêng? Và trường hợp nào buộc phải dùng file riêng?

Đáp án

Return teardown function từ globalSetup gọn hơn — biến server được closure capture, không cần truyền reference qua file. Dùng file globalTeardown riêng buộc thiết khi: teardown logic phức tạp, cần chia sẻ với các project khác, hoặc team convention yêu cầu tách biệt rõ ràng. Một trường hợp đặc biệt: nếu globalSetup throw exception (không return được), teardown function không được đăng ký — file globalTeardown riêng cũng không được gọi trong trường hợp này vì run đã abort.

Câu 5. globalSetup có thể dùng fixture page từ test runner không? Nếu không, phải làm gì để tạo một page trong globalSetup?

Đáp án

Không. globalSetup chạy trong main process trước khi worker nào được spawn — fixture system của Playwright hoàn toàn không khả dụng. Để tạo page, phải dùng Playwright Library API thủ công: const browser = await chromium.launch()const context = await browser.newContext()const page = await context.newPage(). Sau khi xong phải gọi await browser.close() thủ công. Đây là điểm khác biệt cơ bản so với setup project — setup project là test thực sự chạy trong worker nên có đầy đủ fixture.