20 KiB
项目和依赖关系
使用时机:通过单个配置文件跨多个浏览器、设备或环境运行测试。项目(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 })
})
依赖关系如何工作:
- Playwright 识别出没有依赖的项目,并首先运行它们。
- 一旦依赖项目成功完成,依赖它的项目便开始运行。
- 如果依赖项目失败,所有依赖它的项目都会被跳过。
模式 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:自定义 testDir 和 testMatch 的项目
使用时机:不同项目需要不同的测试目录或文件匹配模式。 避免时机:所有项目运行相同的测试。
// 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}` },
})
})
执行顺序:
setup项目运行chromium项目运行(依赖于 setup)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,
},
],
})
# CI:PR 上运行冒烟测试
npx playwright test --project=smoke
# CI:主分支上运行完整回归测试
npx playwright test --project=regression-chromium --project=regression-firefox
# CI:每晚运行慢速测试
npx playwright test --project=slow
决策指南
| 场景 | 项目数量 | 配置 |
|---|---|---|
| 入门阶段 | 1(chromium) | 单个项目,无依赖 |
| 跨浏览器测试 | 3(chromium、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 运行子集 |
| 设置项目修改了数据库但没有清理 | 下次运行状态不干净 | 始终将设置与清理配对,或使设置幂等 |
| 多个项目使用重叠的测试目录 | 同一测试无意中多次运行 | 为每个项目设置明确的 testDir 和 testMatch |
调试时不使用 --no-deps |
每次运行设置,即使只测一个测试 | 在专注调试时使用 --no-deps 跳过依赖 |
故障排除
运行特定项目时提示"未找到测试"
原因:项目的 testDir 或 testMatch 与任何文件都不匹配。
修复:列出项目的测试以查看匹配情况:
npx playwright test --project=chromium --list
检查 testDir 和 testMatch 是否正确:
{
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'] } // 错误——大小写不匹配
来自多个项目的测试相互干扰
原因:项目共享数据库或外部状态。由于项目默认并行运行,它们可能会发生冲突。
修复:要么:
- 使用每个项目隔离的数据(不同的测试用户、不同的数据库 schema)
- 通过添加人为的依赖关系来串行运行项目:
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,
}
相关文档
- core/configuration.md——基础配置、
projects、use设置 - ci/global-setup-teardown.md——
globalSetup与设置项目的对比 - core/fixtures-and-hooks.md——按项目配置的选项 fixture
- core/authentication.md——通过设置项目实现认证状态
- ci/parallel-and-sharding.md——跨项目分片
- ci/ci-github-actions.md——在 CI 中运行特定项目