项目文件夹

文件
2026-07-13 21:36:47 +08:00

18 KiB

全局设置与拆卸

适用场景:必须在整个测试套件之前或之后运行的一次性操作——数据库数据播种、环境健康检查、创建共享认证状态、启动外部服务。每次 npx playwright test 调用运行一次,而非每个测试或每个 worker 运行一次。

快速参考

globalSetup         →  在所有项目的所有测试之前运行一次
  ↓
setup 项目          →  在依赖项目之前运行(拥有浏览器上下文)
  ↓
test 项目           →  你的实际测试
  ↓
teardown 项目       →  在依赖项目之后运行(拥有浏览器上下文)
  ↓
globalTeardown      →  在所有项目的所有测试之后运行一次

关键区别:

  • globalSetup / globalTeardown:无浏览器,无 Playwright fixture。纯 Node.js。
  • 带有 dependencies 的 setup 项目:拥有完整的浏览器上下文,可以使用 pagerequest 等。

模式

模式 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 项目 需要 pagecontext(在 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} 创建`)
  }
})

相关文档