18 KiB
全局设置与拆卸
适用场景:必须在整个测试套件之前或之后运行的一次性操作——数据库数据播种、环境健康检查、创建共享认证状态、启动外部服务。每次
npx playwright test调用运行一次,而非每个测试或每个 worker 运行一次。
快速参考
globalSetup → 在所有项目的所有测试之前运行一次
↓
setup 项目 → 在依赖项目之前运行(拥有浏览器上下文)
↓
test 项目 → 你的实际测试
↓
teardown 项目 → 在依赖项目之后运行(拥有浏览器上下文)
↓
globalTeardown → 在所有项目的所有测试之后运行一次
关键区别:
globalSetup/globalTeardown:无浏览器,无 Playwright fixture。纯 Node.js。- 带有
dependencies的 setup 项目:拥有完整的浏览器上下文,可以使用page、request等。
模式
模式 1:基本的全局设置与拆卸
适用场景:一次性非浏览器工作,如数据库数据播种、环境验证或外部服务准备。 避免场景:你需要浏览器(应改用 setup 项目)或需要每个测试的隔离(应改用 fixture)。
// playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
globalSetup: "./tests/global-setup.ts",
globalTeardown: "./tests/global-teardown.ts",
testDir: "./tests",
})
// 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
// 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,它已经执行了健康检查。
// 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 项目)。
// 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
// 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:环境变量(最简单):
// 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
// 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:共享文件(适用于复杂数据):
// 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
// 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)
}
// 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 项目。
// 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"],
},
],
})
// 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)。
// 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
// 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 的顶层设置环境变量,在任何可能导致延迟的异步工作之前:
// tests/global-setup.ts
async function globalSetup() {
// 这样可行——在返回之前设置
process.env.TEST_RUN_ID = `run-${Date.now()}`
}
export default globalSetup
如果环境变量仍然缺失,改为将数据写入文件(模式 4,方法 2)。
全局设置失败,提示"Cannot find module"
原因:globalSetup 中的路径是相对于配置文件,但模块解析错误。
修复:使用相对于项目根目录的路径:
// playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
globalSetup: "./tests/global-setup.ts", // 相对于配置文件位置
globalTeardown: "./tests/global-teardown.ts",
})
测试失败后全局拆卸未运行
原因:如果进程被杀死(SIGKILL、OOM)而非正常退出,拆卸会被跳过。
修复:将你的设置设计为幂等的。全局设置应能处理上一次未完成的运行留下的脏状态:
// 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:
# 通过不带依赖的方式运行来跳过 setup
npx playwright test --project=chromium --no-deps tests/specific-test.spec.ts
测试启动时 storageState 文件不存在
原因:创建该文件的 setup 项目或 globalSetup 静默失败了,或者路径错误。
修复:添加显式错误处理并验证文件存在:
// 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 —— 每个测试和每个 worker 的 fixture(在大多数情况下优于 globalSetup)
- core/configuration.md ——
globalSetup、globalTeardown、webServer配置 - ci/projects-and-dependencies.md —— 带有
dependencies的 setup 项目 - core/authentication.md —— 使用 setup 项目的认证模式
- core/test-data-management.md —— 数据播种与测试数据管理