Files
skillhub-147-playwright-skill/ci/projects-and-dependencies.md
2026-07-13 21:36:47 +08:00

20 KiB
Raw Permalink Blame History

项目和依赖关系

使用时机:通过单个配置文件跨多个浏览器、设备或环境运行测试。项目(Projects)允许你定义不同的测试配置,这些配置可以相互依赖、共享设置工作并有选择性地运行。

快速参考

# 运行所有项目
npx playwright test

# 运行特定项目
npx playwright test --project=chromium
npx playwright test --project="Mobile Safari"

# 运行多个项目
npx playwright test --project=chromium --project=firefox

# 列出所有项目及其测试
npx playwright test --list

# 跳过依赖(例如,调试时跳过设置)
npx playwright test --project=chromium --no-deps

模式

模式 1:多浏览器测试

使用时机:确保你的应用能在 Chromium、Firefox 和 WebKit 上正常工作。 避免时机:早期开发阶段——先只使用 Chromium,后续再添加浏览器。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },
  ],
})

CI 中有选择性地进行浏览器测试(PR 快速,主分支全面):

# .github/workflows/ci.yml
jobs:
  test-pr:
    if: github.event_name == 'pull_request'
    steps:
      # PR 上仅运行 Chromium,获取快速反馈
      - run: npx playwright test --project=chromium

  test-main:
    if: github.ref == 'refs/heads/main'
    steps:
      # 主分支上运行所有浏览器
      - run: npx playwright test

模式 2:桌面端和移动端项目

使用时机:测试响应式布局、触摸交互或移动端特定行为。 避免时机:你的应用仅限桌面端。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,

  projects: [
    // 桌面端浏览器
    {
      name: "Desktop Chrome",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "Desktop Firefox",
      use: { ...devices["Desktop Firefox"] },
    },
    {
      name: "Desktop Safari",
      use: { ...devices["Desktop Safari"] },
    },

    // 移动设备
    {
      name: "Mobile Chrome",
      use: { ...devices["Pixel 7"] },
    },
    {
      name: "Mobile Safari",
      use: { ...devices["iPhone 14"] },
    },

    // 平板
    {
      name: "iPad",
      use: { ...devices["iPad Pro 11"] },
    },
  ],
})

仅运行移动端或仅运行桌面端:

npx playwright test --project="Mobile Chrome" --project="Mobile Safari"
npx playwright test --project="Desktop Chrome" --project="Desktop Firefox"

模式 3:带依赖关系的设置项目

使用时机:测试需要共享状态(身份认证、预置数据),且该状态应在所有测试项目运行前一次性创建完成。 避免时机:测试完全独立,没有共享设置阶段。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  testDir: "./tests",

  projects: [
    // 设置首先运行——没有依赖,因此立即执行
    {
      name: "setup",
      testMatch: /global\.setup\.ts/,
    },

    // 浏览器项目依赖于设置
    {
      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()
  await page.context().storageState({ path: authFile })
})

依赖关系如何工作:

  1. Playwright 识别出没有依赖的项目,并首先运行它们。
  2. 一旦依赖项目成功完成,依赖它的项目便开始运行。
  3. 如果依赖项目失败,所有依赖它的项目都会被跳过。

模式 4:多个认证角色

使用时机:测试需要不同用户角色(管理员、编辑者、查看者)且各自拥有独立的认证状态。 避免时机:所有测试使用同一用户——使用单个设置项目即可。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  testDir: "./tests",

  projects: [
    // 每个角色的认证设置
    {
      name: "auth-admin",
      testMatch: /auth\.setup\.ts/,
      use: {
        userRole: "admin",
        storageStatePath: "playwright/.auth/admin.json",
      },
    },
    {
      name: "auth-editor",
      testMatch: /auth\.setup\.ts/,
      use: {
        userRole: "editor",
        storageStatePath: "playwright/.auth/editor.json",
      },
    },

    // 管理员测试
    {
      name: "admin-tests",
      testDir: "./tests/admin",
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/admin.json",
      },
      dependencies: ["auth-admin"],
    },

    // 编辑者测试
    {
      name: "editor-tests",
      testDir: "./tests/editor",
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/editor.json",
      },
      dependencies: ["auth-editor"],
    },

    // 未认证测试(无依赖,无 storageState
    {
      name: "public-tests",
      testDir: "./tests/public",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
})
// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test"

const credentials: Record<string, { email: string; password: string }> = {
  admin: { email: "admin@example.com", password: process.env.ADMIN_PASSWORD! },
  editor: { email: "editor@example.com", password: process.env.EDITOR_PASSWORD! },
}

setup("authenticate", async ({ page }, testInfo) => {
  const role = testInfo.project.use.userRole as string
  const authFile = testInfo.project.use.storageStatePath as string
  const { email, password } = credentials[role]

  await page.goto("/login")
  await page.getByLabel("Email").fill(email)
  await page.getByLabel("Password").fill(password)
  await page.getByRole("button", { name: "Sign in" }).click()
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
  await page.context().storageState({ path: authFile })
})

模式 5:环境特定项目

使用时机:通过一个配置文件针对不同环境(开发、预发布、生产)运行相同的测试。 避免时机:你只测试一个环境。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

const ENV = process.env.TEST_ENV || "local"

const envConfig: Record<string, { baseURL: string; retries: number }> = {
  local: { baseURL: "http://localhost:3000", retries: 0 },
  staging: { baseURL: "https://staging.example.com", retries: 2 },
  production: { baseURL: "https://www.example.com", retries: 2 },
}

const env = envConfig[ENV]

export default defineConfig({
  testDir: "./tests",
  retries: env.retries,
  use: {
    baseURL: env.baseURL,
  },

  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
    // 仅在生产环境运行冒烟测试
    ...(ENV === "production"
      ? [
          {
            name: "smoke",
            testMatch: "**/*smoke*.spec.ts",
            use: { ...devices["Desktop Chrome"] },
            grep: /@smoke/,
          },
        ]
      : []),
  ],
})
# 针对不同环境运行
TEST_ENV=local npx playwright test
TEST_ENV=staging npx playwright test
TEST_ENV=production npx playwright test --project=smoke

模式 6:自定义 testDirtestMatch 的项目

使用时机:不同项目需要不同的测试目录或文件匹配模式。 避免时机:所有项目运行相同的测试。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  projects: [
    // E2E 测试——在所有浏览器中运行
    {
      name: "e2e-chromium",
      testDir: "./tests/e2e",
      testMatch: "**/*.spec.ts",
      use: { ...devices["Desktop Chrome"] },
    },
    {
      name: "e2e-firefox",
      testDir: "./tests/e2e",
      testMatch: "**/*.spec.ts",
      use: { ...devices["Desktop Firefox"] },
    },

    // API 测试——无需浏览器,运行一次即可
    {
      name: "api",
      testDir: "./tests/api",
      testMatch: "**/*.spec.ts",
      use: {
        baseURL: "https://api.example.com",
      },
    },

    // 视觉回归测试——仅 Chromium
    {
      name: "visual",
      testDir: "./tests/visual",
      testMatch: "**/*.spec.ts",
      use: {
        ...devices["Desktop Chrome"],
        // 锁定视口以获取一致的截图
        viewport: { width: 1280, height: 720 },
      },
    },
  ],
})
# 仅运行 API 测试
npx playwright test --project=api

# 仅运行视觉测试
npx playwright test --project=visual

# 运行所有 E2E 测试
npx playwright test --project=e2e-chromium --project=e2e-firefox

模式 7:清理(Teardown)项目

使用时机:测试完成后需要基于浏览器的清理工作(例如,通过 UI 删除测试数据)。 避免时机:可以通过 API 在 globalTeardown 或 fixture 中完成清理(通常情况)。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  projects: [
    {
      name: "setup",
      testMatch: /global\.setup\.ts/,
      teardown: "teardown", // 链接到清理项目
    },
    {
      name: "teardown",
      testMatch: /global\.teardown\.ts/,
    },
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        storageState: "playwright/.auth/user.json",
      },
      dependencies: ["setup"],
    },
  ],
})
// tests/global.teardown.ts
import { test as teardown } from "@playwright/test"

teardown("clean up test data", async ({ request }) => {
  await request.post("/api/test/cleanup", {
    headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` },
  })
})

执行顺序:

  1. setup 项目运行
  2. chromium 项目运行(依赖于 setup
  3. teardown 项目运行(通过 setup 的 teardown 字段链接)

模式 8:使用 Grep 和 GrepInvert 进行项目过滤

使用时机:不同项目应基于标签运行不同的测试子集。 避免时机:所有项目运行所有测试。

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
  testDir: "./tests",

  projects: [
    // 冒烟测试——仅 @smoke 标记的测试,仅 Chromium
    {
      name: "smoke",
      use: { ...devices["Desktop Chrome"] },
      grep: /@smoke/,
    },

    // 完整回归测试——除 @slow 之外的所有测试,所有浏览器
    {
      name: "regression-chromium",
      use: { ...devices["Desktop Chrome"] },
      grepInvert: /@slow/,
    },
    {
      name: "regression-firefox",
      use: { ...devices["Desktop Firefox"] },
      grepInvert: /@slow/,
    },

    // 慢速测试——仅 @slow,仅 Chromium,更高超时
    {
      name: "slow",
      use: { ...devices["Desktop Chrome"] },
      grep: /@slow/,
      timeout: 120_000,
    },
  ],
})
# CIPR 上运行冒烟测试
npx playwright test --project=smoke

# CI:主分支上运行完整回归测试
npx playwright test --project=regression-chromium --project=regression-firefox

# CI:每晚运行慢速测试
npx playwright test --project=slow

决策指南

场景 项目数量 配置
入门阶段 1chromium 单个项目,无依赖
跨浏览器测试 3chromium、firefox、webkit 每个浏览器引擎一个项目
响应式设计 5-6(桌面端 + 移动端 + 平板) 使用 devices 预设
需要认证的测试 1 个设置 + N 个浏览器项目 使用 dependencies 的设置项目
多个认证角色 N 个设置 + N 个测试项目 每个角色一个设置项目
不同类型的测试(E2E、API、视觉) 每种类型一个 每个项目自定义 testDir
环境目标 相同项目,不同 baseURL 使用 TEST_ENV 环境变量
冒烟测试 vs 回归测试套件 带有 grep / grepInvert 的项目 基于标签的过滤
特性 dependencies teardown globalSetup
是否有浏览器上下文
何时运行 在依赖它的项目之前 在链接的设置项目的依赖项目之后 在所有项目之前
用于 认证状态、带浏览器的数据预置 基于浏览器的清理 非浏览器设置(数据库、健康检查)
依赖失败时是否跳过 不适用 是(整个套件失败)

反模式

反模式 问题 应改为
每个 PR 上都运行所有浏览器 3 倍 CI 时间,收益甚微 PR 上仅 Chromium;主分支上运行所有浏览器
在不需要共享状态的项目上使用 dependencies 强制串行执行;速度变慢 仅当项目真正需要共享状态时才使用依赖关系
跨项目重复配置 难以维护;配置项容易偏离 在顶层共享 use 配置;按项目覆盖
CI 中不使用 --project 过滤 所有项目始终运行;无法控制 根据上下文使用 --project 运行子集
设置项目修改了数据库但没有清理 下次运行状态不干净 始终将设置与清理配对,或使设置幂等
多个项目使用重叠的测试目录 同一测试无意中多次运行 为每个项目设置明确的 testDirtestMatch
调试时不使用 --no-deps 每次运行设置,即使只测一个测试 在专注调试时使用 --no-deps 跳过依赖

故障排除

运行特定项目时提示"未找到测试"

原因:项目的 testDirtestMatch 与任何文件都不匹配。

修复:列出项目的测试以查看匹配情况:

npx playwright test --project=chromium --list

检查 testDirtestMatch 是否正确:

{
  name: 'chromium',
  testDir: './tests',         // 必须包含测试文件
  testMatch: '**/*.spec.ts',  // 必须匹配你的文件命名
}

即使只运行单个测试,设置项目也每次都执行

原因:测试的项目配置了 dependencies: ['setup'],因此设置总是先运行。

修复:在开发时使用 --no-deps 跳过依赖:

npx playwright test --project=chromium --no-deps tests/specific-test.spec.ts

storageState 文件未找到

原因:设置项目失败或未创建该文件。或者 use.storageState 中的路径与设置项目中的路径不匹配。

修复:验证路径完全一致:

// 在设置测试中:
await page.context().storageState({ path: "playwright/.auth/user.json" })

// 在项目配置中:
use: {
  storageState: "playwright/.auth/user.json"
} // 必须完全匹配

.auth 添加到 .gitignore

echo "playwright/.auth/" >> .gitignore

即使设置失败,依赖项目仍然运行

原因:这种情况不应该发生——当依赖失败时,Playwright 会跳过依赖项目。如果看起来它们仍在运行,请检查 dependencies 的拼写是否正确。

修复:验证依赖名称完全匹配:

// 设置项目名称
{ name: 'setup', ... }

// 依赖引用(必须完全匹配,区分大小写)
{ dependencies: ['setup'] }  // 正确
{ dependencies: ['Setup'] }  // 错误——大小写不匹配

来自多个项目的测试相互干扰

原因:项目共享数据库或外部状态。由于项目默认并行运行,它们可能会发生冲突。

修复:要么:

  1. 使用每个项目隔离的数据(不同的测试用户、不同的数据库 schema)
  2. 通过添加人为的依赖关系来串行运行项目:
projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  {
    name: 'firefox',
    use: { ...devices['Desktop Firefox'] },
    dependencies: ['chromium'],  // 强制串行执行
  },
],

自定义 use 属性导致 TypeScript 错误

原因Playwright 的 use 类型不包含你的自定义属性。

修复:扩展类型或使用 as any

// 选项 1:声明自定义属性
declare module '@playwright/test' {
  interface PlaywrightTestOptions {
    userRole: string;
    storageStatePath: string;
  }
}

// 选项 2:快速修复(类型安全性较低)
{
  name: 'auth-admin',
  use: {
    userRole: 'admin',
    storageStatePath: 'playwright/.auth/admin.json',
  } as any,
}

相关文档