# 全局设置与拆卸 > **适用场景**:必须在整个测试套件之前或之后运行的**一次性操作**——数据库数据播种、环境健康检查、创建共享认证状态、启动外部服务。每次 `npx playwright test` 调用运行一次,而非每个测试或每个 worker 运行一次。 ## 快速参考 ``` globalSetup → 在所有项目的所有测试之前运行一次 ↓ setup 项目 → 在依赖项目之前运行(拥有浏览器上下文) ↓ test 项目 → 你的实际测试 ↓ teardown 项目 → 在依赖项目之后运行(拥有浏览器上下文) ↓ globalTeardown → 在所有项目的所有测试之后运行一次 ``` **关键区别:** - `globalSetup` / `globalTeardown`:无浏览器,无 Playwright fixture。纯 Node.js。 - 带有 `dependencies` 的 setup 项目:拥有完整的浏览器上下文,可以使用 `page`、`request` 等。 ## 模式 ### 模式 1:基本的全局设置与拆卸 **适用场景**:一次性非浏览器工作,如数据库数据播种、环境验证或外部服务准备。 **避免场景**:你需要浏览器(应改用 setup 项目)或需要每个测试的隔离(应改用 fixture)。 ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ globalSetup: "./tests/global-setup.ts", globalTeardown: "./tests/global-teardown.ts", testDir: "./tests", }) ``` ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" async function globalSetup(config: FullConfig) { console.log("全局设置:正在播种数据库...") // 播种测试数据库 const { execSync } = await import("child_process") execSync("npx prisma db push --force-reset", { stdio: "inherit" }) execSync("npx prisma db seed", { stdio: "inherit" }) // 存储供测试使用的运行元数据 process.env.TEST_RUN_ID = `run-${Date.now()}` } export default globalSetup ``` ```ts // tests/global-teardown.ts import type { FullConfig } from "@playwright/test" async function globalTeardown(config: FullConfig) { console.log("全局拆卸:正在清理...") const { execSync } = await import("child_process") execSync("npx prisma db push --force-reset", { stdio: "inherit" }) } export default globalTeardown ``` ### 模式 2:全局设置中的环境健康检查 **适用场景**:在运行任何测试之前验证测试环境是否健康。如果服务宕机则快速失败。 **避免场景**:测试使用了 `webServer`,它已经执行了健康检查。 ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" async function globalSetup(config: FullConfig) { const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000" const maxRetries = 10 const retryDelay = 2000 console.log(`正在检查 ${baseURL} 是否可达...`) for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(`${baseURL}/api/health`) if (response.ok) { console.log(`环境正常(尝试第 ${i + 1} 次)`) return } } catch { // 连接被拒绝或超时——重试 } console.log(`正在等待环境就绪...(第 ${i + 1}/${maxRetries} 次尝试)`) await new Promise((resolve) => setTimeout(resolve, retryDelay)) } throw new Error(`环境 ${baseURL} 在 ${maxRetries} 次尝试后仍不可达`) } export default globalSetup ``` ### 模式 3:全局设置中的认证状态(无浏览器) **适用场景**:通过 API 创建认证令牌或会话 Cookie,无需浏览器。 **避免场景**:登录需要浏览器交互(应改用 setup 项目)。 ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" import * as fs from "fs" import * as path from "path" async function globalSetup(config: FullConfig) { const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000" // 通过 API 进行认证(无需浏览器) const response = await fetch(`${baseURL}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "admin@example.com", password: process.env.TEST_PASSWORD, }), }) if (!response.ok) { throw new Error(`认证失败:${response.status} ${response.statusText}`) } const { token } = await response.json() // 将令牌保存为 storageState,供浏览器测试使用 const authDir = path.resolve(process.cwd(), "playwright/.auth") fs.mkdirSync(authDir, { recursive: true }) const storageState = { cookies: [], origins: [ { origin: baseURL, localStorage: [{ name: "auth_token", value: token }], }, ], } fs.writeFileSync(path.join(authDir, "user.json"), JSON.stringify(storageState, null, 2)) } export default globalSetup ``` ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ globalSetup: "./tests/global-setup.ts", projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json", }, }, ], }) ``` ### 模式 4:从全局设置向测试传递数据 **适用场景**:全局设置生成了测试所需的数值(ID、令牌、URL)。 **避免场景**:每个测试应创建自己的数据(通常情况下)。 **方法 1:环境变量(最简单):** ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" async function globalSetup(config: FullConfig) { process.env.TEST_RUN_ID = `run-${Date.now()}` process.env.SEED_USER_ID = "user-12345" } export default globalSetup ``` ```ts // tests/dashboard.spec.ts import { test, expect } from "@playwright/test" test("仪表盘显示已播种的数据", async ({ page }) => { const userId = process.env.SEED_USER_ID await page.goto(`/users/${userId}/dashboard`) await expect(page.getByRole("heading")).toBeVisible() }) ``` **方法 2:共享文件(适用于复杂数据):** ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" import * as fs from "fs" import * as path from "path" const SETUP_DATA_PATH = path.resolve(process.cwd(), "test-data/setup-data.json") async function globalSetup(config: FullConfig) { const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000" // 通过 API 创建测试数据 const res = await fetch(`${baseURL}/api/test/seed`, { method: "POST" }) const seedData = await res.json() // 写入共享文件 const dir = path.dirname(SETUP_DATA_PATH) fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(SETUP_DATA_PATH, JSON.stringify(seedData, null, 2)) } export default globalSetup ``` ```ts // tests/helpers/setup-data.ts import * as fs from "fs" import * as path from "path" const SETUP_DATA_PATH = path.resolve(process.cwd(), "test-data/setup-data.json") export function getSetupData(): { userId: string; orgId: string; apiKey: string } { const raw = fs.readFileSync(SETUP_DATA_PATH, "utf8") return JSON.parse(raw) } ``` ```ts // tests/org-settings.spec.ts import { test, expect } from "@playwright/test" import { getSetupData } from "./helpers/setup-data" test("组织设置页面加载", async ({ page }) => { const { orgId } = getSetupData() await page.goto(`/orgs/${orgId}/settings`) await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible() }) ``` ### 模式 5:使用 `storageState` 的全局设置(基于浏览器的认证) **适用场景**:认证需要浏览器交互(表单登录、OAuth 重定向、多因素认证)。 **避免场景**:认证可以通过 API 调用完成(改用模式 3)。 **重要提示**:`globalSetup` 没有浏览器。对于基于浏览器的认证,请改用 **setup 项目**。 ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ projects: [ // Setup 项目:优先运行,拥有浏览器,保存认证状态 { name: "setup", testMatch: /global\.setup\.ts/, }, // 测试项目:依赖于 setup,重用已保存的认证状态 { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, { name: "firefox", use: { ...devices["Desktop Firefox"], storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, ], }) ``` ```ts // tests/global.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("user@example.com") await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!) await page.getByRole("button", { name: "Sign in" }).click() // 等待导航以确认登录成功 await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() // 保存已登录状态(cookies + localStorage) await page.context().storageState({ path: authFile }) }) ``` ### 模式 6:外部服务的全局设置 **适用场景**:在任何测试运行之前启动或配置外部服务(模拟服务器、测试容器、功能开关)。 **避免场景**:需要每个测试或每个 worker 的隔离(应使用 fixture)。 ```ts // tests/global-setup.ts import type { FullConfig } from "@playwright/test" async function globalSetup(config: FullConfig) { // 启动模拟 API 服务器 const { createServer } = await import("../mocks/server") const server = await createServer() const port = await server.listen(0) process.env.MOCK_API_URL = `http://localhost:${port}` // 为测试环境配置功能开关 const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000" await fetch(`${baseURL}/api/admin/feature-flags`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.ADMIN_API_KEY}`, }, body: JSON.stringify({ newCheckout: true, darkMode: false, betaFeatures: true, }), }) // 返回清理函数(Playwright 会单独调用 globalTeardown) // 如需清理,请使用 globalTeardown } export default globalSetup ``` ```ts // tests/global-teardown.ts import type { FullConfig } from "@playwright/test" async function globalTeardown(config: FullConfig) { // 重置功能开关 const baseURL = config.projects[0]?.use?.baseURL || "http://localhost:3000" await fetch(`${baseURL}/api/admin/feature-flags/reset`, { method: "POST", headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` }, }) } export default globalTeardown ``` ## 决策指南 | 需求 | 使用 | 原因 | | ---------------------------------- | --------------------------------------------- | --------------------------------------------------------- | | 一次性数据库播种 | `globalSetup` | 无需浏览器;在所有测试之前运行一次 | | 基于浏览器的登录(共享状态) | 带有 `dependencies` 的 setup 项目 | 需要 `page` 和 `context`(在 globalSetup 中不可用) | | 基于 API 的认证令牌 | `globalSetup` | 简单的 HTTP 调用,无需浏览器 | | 每个测试的独立数据 | 通过 `test.extend()` 的自定义 fixture | 每个测试获得隔离的数据 | | 每个 worker 的共享资源 | Worker 作用域的 fixture(`{ scope: 'worker' }`)| 在 worker 内共享,在 worker 间隔离 | | 测试前的健康检查 | `globalSetup` | 如果环境宕机则快速失败 | | 启动模拟服务器 | `globalSetup` + `globalTeardown` | 一次性服务器生命周期 | | 所有测试结束后清理 | `globalTeardown` | 在最后运行一次,无论通过/失败 | ``` 我需要 globalSetup 吗? │ ├── 工作是否需要浏览器(page、context)? │ ├── 是 → 使用 setup 项目,而非 globalSetup │ └── 否 → globalSetup 合适 │ ├── 每个测试是否需要唯一/隔离的数据? │ ├── 是 → 使用 test.extend() 的 fixture │ └── 否 → globalSetup 用于共享的只读数据 │ ├── 是否为每个 worker 所需(昂贵资源、连接池)? │ ├── 是 → Worker 作用域的 fixture │ └── 否 → globalSetup 用于真正的全局一次性工作 │ └── 是否为清理工作? ├── 所有测试之后 → globalTeardown ├── 每个测试之后 → Fixture 拆卸(after use()) └── 每个 worker 之后 → Worker 作用域的 fixture 拆卸 ``` ## 反模式 | 反模式 | 问题 | 应改为 | | ------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------- | | 在 `globalSetup` 中进行浏览器登录 | 没有浏览器上下文可用;需要复杂的变通方案 | 使用带有 `dependencies` 的 setup 项目 | | 在 `globalSetup` 中创建每个测试的数据 | 所有测试共享相同的数据;没有隔离 | 使用每个测试的 fixture 来创建独立数据 | | 只有 `globalSetup` 没有 `globalTeardown` | 数据库或服务处于脏状态 | 始终将 setup 与 teardown 配对 | | 将设置结果存储在模块级变量中 | Worker 是独立的进程;变量不共享 | 使用环境变量或文件 | | 在 `globalSetup` 中包含复杂逻辑 | 难以调试;在正常测试生命周期之外运行 | 保持简洁:播种、验证、设置环境变量 | | `globalSetup` 耗时超过 60 秒 | 拖慢每次测试运行,即使只运行一个测试 | 将繁重工作移到单独的脚本或 CI 步骤中 | | 依赖 `globalTeardown` 执行关键清理 | 如果进程崩溃,`globalTeardown` 可能不会运行 | 将测试设计为幂等的;在 setup 项目中使用 `beforeAll` | ## 故障排查 ### 全局设置已运行,但环境变量在测试中不可用 **原因**:每个 worker 是一个独立进程。`globalSetup` 中对 `process.env` 的修改会传播给 worker,但前提是在 worker 生成之前设置。 **修复**:在 `globalSetup` 的顶层设置环境变量,在任何可能导致延迟的异步工作之前: ```ts // tests/global-setup.ts async function globalSetup() { // 这样可行——在返回之前设置 process.env.TEST_RUN_ID = `run-${Date.now()}` } export default globalSetup ``` 如果环境变量仍然缺失,改为将数据写入文件(模式 4,方法 2)。 ### 全局设置失败,提示"Cannot find module" **原因**:`globalSetup` 中的路径是相对于配置文件,但模块解析错误。 **修复**:使用相对于项目根目录的路径: ```ts // playwright.config.ts import { defineConfig } from "@playwright/test" export default defineConfig({ globalSetup: "./tests/global-setup.ts", // 相对于配置文件位置 globalTeardown: "./tests/global-teardown.ts", }) ``` ### 测试失败后全局拆卸未运行 **原因**:如果进程被杀死(SIGKILL、OOM)而非正常退出,拆卸会被跳过。 **修复**:将你的设置设计为幂等的。全局设置应能处理上一次未完成的运行留下的脏状态: ```ts // tests/global-setup.ts async function globalSetup() { // 总是先重置,再播种——处理脏状态 const { execSync } = await import("child_process") execSync("npx prisma db push --force-reset", { stdio: "inherit" }) execSync("npx prisma db seed", { stdio: "inherit" }) } export default globalSetup ``` ### Setup 项目每次都会运行,即使只运行一个测试文件 **原因**:`dependencies` 配置要求 setup 项目在任何依赖项目之前运行。 **修复**:这是预期行为。要在专注调试时跳过 setup: ```bash # 通过不带依赖的方式运行来跳过 setup npx playwright test --project=chromium --no-deps tests/specific-test.spec.ts ``` ### 测试启动时 `storageState` 文件不存在 **原因**:创建该文件的 setup 项目或 `globalSetup` 静默失败了,或者路径错误。 **修复**:添加显式错误处理并验证文件存在: ```ts // tests/global.setup.ts import { test as setup, expect } from "@playwright/test" import * as fs from "fs" const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!) await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await page.context().storageState({ path: authFile }) // 验证文件已创建 if (!fs.existsSync(authFile)) { throw new Error(`认证状态文件未在 ${authFile} 创建`) } }) ``` ## 相关文档 - [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) —— 每个测试和每个 worker 的 fixture(在大多数情况下优于 globalSetup) - [core/configuration.md](../core/configuration.md) —— `globalSetup`、`globalTeardown`、`webServer` 配置 - [ci/projects-and-dependencies.md](projects-and-dependencies.md) —— 带有 `dependencies` 的 setup 项目 - [core/authentication.md](../core/authentication.md) —— 使用 setup 项目的认证模式 - [core/test-data-management.md](../core/test-data-management.md) —— 数据播种与测试数据管理