Files
2026-07-13 21:36:47 +08:00

29 KiB
Raw Permalink Blame History

测试组织

何时使用:组织测试文件、命名测试、使用 describe 分组、打标签、筛选,以及决定并行与串行执行。 前置知识core/configuration.md

快速参考

概念 规则
文件后缀 .spec.ts / .spec.js — 始终如此
分组 按特性/领域,而不是按页面或 URL
测试名称 test('should ...')test('user can ...') — 描述行为
嵌套层级 最多 2 层 test.describe()
默认执行方式 fullyParallel: true — 默认并行运行测试
串行测试 几乎不使用 test.describe.serial()
测试依赖 避免——每个测试自己设置状态
标签 @smoke@regression@slow——使用 --grep 筛选

模式

模式 1:基于特性的文件结构

何时使用:任何项目——此为默认布局。 何时避免:从不。这始终是正确的。

按特性或领域分组测试,而不是按页面类或测试类型。

小型项目(< 20 个测试):

tests/
├── auth.spec.ts
├── dashboard.spec.ts
├── settings.spec.ts
└── checkout.spec.ts
playwright.config.ts

中型项目(20200 个测试):

tests/
├── auth/
│   ├── login.spec.ts
│   ├── signup.spec.ts
│   ├── password-reset.spec.ts
│   └── mfa.spec.ts
├── dashboard/
│   ├── widgets.spec.ts
│   ├── filters.spec.ts
│   └── export.spec.ts
├── checkout/
│   ├── cart.spec.ts
│   ├── payment.spec.ts
│   └── confirmation.spec.ts
├── settings/
│   ├── profile.spec.ts
│   └── notifications.spec.ts
└── fixtures/
    ├── auth.fixture.ts
    └── test-data.ts
playwright.config.ts

大型项目(200+ 个测试):

tests/
├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   ├── signup.spec.ts
│   │   ├── password-reset.spec.ts
│   │   └── mfa.spec.ts
│   ├── checkout/
│   │   ├── cart.spec.ts
│   │   ├── payment.spec.ts
│   │   ├── promo-codes.spec.ts
│   │   └── confirmation.spec.ts
│   ├── admin/
│   │   ├── user-management.spec.ts
│   │   ├── reports.spec.ts
│   │   └── audit-log.spec.ts
│   └── inventory/
│       ├── product-list.spec.ts
│       ├── product-detail.spec.ts
│       └── stock-management.spec.ts
├── api/
│   ├── users.spec.ts
│   ├── orders.spec.ts
│   └── products.spec.ts
├── visual/
│   ├── homepage.spec.ts
│   └── product-page.spec.ts
├── fixtures/
│   ├── auth.fixture.ts
│   ├── db.fixture.ts
│   └── base.fixture.ts
├── page-objects/
│   ├── login.page.ts
│   ├── dashboard.page.ts
│   └── checkout.page.ts
└── helpers/
    ├── test-data.ts
    └── api-client.ts
playwright.config.ts

模式 2:命名约定

何时使用:编写任何测试或文件时。 何时避免:从不。

TypeScript:

// tests/checkout/cart.spec.ts
import { test, expect } from "@playwright/test"

// 好:按特性分组,描述行为
test.describe("Shopping Cart", () => {
  test("should add item to empty cart", async ({ page }) => {
    await page.goto("/products/widget-a")
    await page.getByRole("button", { name: "Add to cart" }).click()
    await expect(page.getByTestId("cart-count")).toHaveText("1")
  })

  test("should update quantity when same item added twice", async ({ page }) => {
    await page.goto("/products/widget-a")
    await page.getByRole("button", { name: "Add to cart" }).click()
    await page.getByRole("button", { name: "Add to cart" }).click()
    await expect(page.getByTestId("cart-count")).toHaveText("2")
  })

  test("user can remove item from cart", async ({ page }) => {
    await page.goto("/cart")
    // ... 通过 API 或 fixture 在购物车中设置商品
    await page.getByRole("button", { name: "Remove" }).first().click()
    await expect(page.getByText("Your cart is empty")).toBeVisible()
  })
})

JavaScript:

// tests/checkout/cart.spec.js
const { test, expect } = require("@playwright/test")

test.describe("Shopping Cart", () => {
  test("should add item to empty cart", async ({ page }) => {
    await page.goto("/products/widget-a")
    await page.getByRole("button", { name: "Add to cart" }).click()
    await expect(page.getByTestId("cart-count")).toHaveText("1")
  })

  test("should update quantity when same item added twice", async ({ page }) => {
    await page.goto("/products/widget-a")
    await page.getByRole("button", { name: "Add to cart" }).click()
    await page.getByRole("button", { name: "Add to cart" }).click()
    await expect(page.getByTestId("cart-count")).toHaveText("2")
  })

  test("user can remove item from cart", async ({ page }) => {
    await page.goto("/cart")
    await page.getByRole("button", { name: "Remove" }).first().click()
    await expect(page.getByText("Your cart is empty")).toBeVisible()
  })
})

命名规则:

元素 约定 示例
文件名 kebab-case.spec.ts password-reset.spec.ts
test.describe() 首字母大写,特性名称 'Password Reset'
test() shoulduser can 开头的句子 'should send reset email'
页面对象 PascalCase.page.ts login.page.ts / LoginPage
Fixtures kebab-case.fixture.ts auth.fixture.ts

模式 3test.describe() 分组

何时使用:一个文件中有多个相关测试,共享上下文或设置。 何时避免:一个文件只有 1–3 个测试且无共享设置——跳过 describe 包装。

TypeScript:

// tests/auth/login.spec.ts
import { test, expect } from "@playwright/test"

test.describe("Login", () => {
  // 该 describe 块中所有测试的共享设置
  test.beforeEach(async ({ page }) => {
    await page.goto("/login")
  })

  test("should login with valid credentials", async ({ page }) => {
    await page.getByLabel("Email").fill("user@example.com")
    await page.getByLabel("Password").fill("securepass123")
    await page.getByRole("button", { name: "Sign in" }).click()
    await expect(page).toHaveURL("/dashboard")
  })

  test("should show error for invalid password", async ({ page }) => {
    await page.getByLabel("Email").fill("user@example.com")
    await page.getByLabel("Password").fill("wrongpassword")
    await page.getByRole("button", { name: "Sign in" }).click()
    await expect(page.getByRole("alert")).toHaveText("Invalid credentials")
  })

  // 一层嵌套——可接受
  test.describe("Rate Limiting", () => {
    test("should lock account after 5 failed attempts", async ({ page }) => {
      for (let i = 0; i < 5; i++) {
        await page.getByLabel("Email").fill("user@example.com")
        await page.getByLabel("Password").fill("wrong")
        await page.getByRole("button", { name: "Sign in" }).click()
      }
      await expect(page.getByRole("alert")).toContainText("Account locked")
    })
  })
})

JavaScript:

// tests/auth/login.spec.js
const { test, expect } = require("@playwright/test")

test.describe("Login", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/login")
  })

  test("should login with valid credentials", async ({ page }) => {
    await page.getByLabel("Email").fill("user@example.com")
    await page.getByLabel("Password").fill("securepass123")
    await page.getByRole("button", { name: "Sign in" }).click()
    await expect(page).toHaveURL("/dashboard")
  })

  test("should show error for invalid password", async ({ page }) => {
    await page.getByLabel("Email").fill("user@example.com")
    await page.getByLabel("Password").fill("wrongpassword")
    await page.getByRole("button", { name: "Sign in" }).click()
    await expect(page.getByRole("alert")).toHaveText("Invalid credentials")
  })

  test.describe("Rate Limiting", () => {
    test("should lock account after 5 failed attempts", async ({ page }) => {
      for (let i = 0; i < 5; i++) {
        await page.getByLabel("Email").fill("user@example.com")
        await page.getByLabel("Password").fill("wrong")
        await page.getByRole("button", { name: "Sign in" }).click()
      }
      await expect(page.getByRole("alert")).toContainText("Account locked")
    })
  })
})

嵌套限制:最多 2 层。 如果需要第三层,请拆分到单独的文件。


模式 4:标签与注解

何时使用:需要对测试进行分类以实现选择性运行(冒烟测试套件、CI 流水线、跳过已知问题)。 何时避免:所有测试总是一起运行,且测试数量 < 20。

TypeScript:

// tests/checkout/payment.spec.ts
import { test, expect } from "@playwright/test"

// 在测试标题中打标签——最简单的方法
test("should process credit card payment @smoke", async ({ page }) => {
  await page.goto("/checkout")
  await page.getByLabel("Card number").fill("4242424242424242")
  await page.getByLabel("Expiry").fill("12/28")
  await page.getByLabel("CVC").fill("123")
  await page.getByRole("button", { name: "Pay now" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible()
})

test("should handle declined card @regression", async ({ page }) => {
  await page.goto("/checkout")
  await page.getByLabel("Card number").fill("4000000000000002")
  await page.getByLabel("Expiry").fill("12/28")
  await page.getByLabel("CVC").fill("123")
  await page.getByRole("button", { name: "Pay now" }).click()
  await expect(page.getByRole("alert")).toContainText("Card declined")
})

// 通过 test.describe 进行组级标签
test.describe("Payment Edge Cases @regression", () => {
  test("should handle network timeout during payment", async ({ page }) => {
    await page.goto("/checkout")
    // ... 测试实现
    await expect(page.getByText("Please try again")).toBeVisible()
  })
})

// 用于测试生命周期控制的注解
test("should render 3D Secure iframe @slow", async ({ page }) => {
  test.slow() // 将默认超时时间延长至三倍
  await page.goto("/checkout/3ds")
  // ... 耗时较长的 3D Secure 流程
  await expect(page.getByText("Verified")).toBeVisible()
})

test("should apply loyalty points discount", async ({ page }) => {
  test.skip(process.env.CI !== "true", "Only run in CI — needs loyalty service")
  await page.goto("/checkout")
  // ...
})

test("should handle Apple Pay on Safari", async ({ page, browserName }) => {
  test.skip(browserName !== "webkit", "Apple Pay only works in Safari")
  await page.goto("/checkout")
  // ...
})

test("should display PayPal button", async ({ page }) => {
  test.fixme() // 已知问题——在 JIRA-1234 中跟踪
  await page.goto("/checkout")
  await expect(page.getByRole("button", { name: "PayPal" })).toBeVisible()
})

test("should fail when submitting empty card form", async ({ page }) => {
  test.fail() // 此测试预期失败——记录已知错误
  await page.goto("/checkout")
  await page.getByRole("button", { name: "Pay now" }).click()
  // 错误:目前没有显示验证——此断言将失败
  await expect(page.getByRole("alert")).toBeVisible()
})

JavaScript:

// tests/checkout/payment.spec.js
const { test, expect } = require("@playwright/test")

test("should process credit card payment @smoke", async ({ page }) => {
  await page.goto("/checkout")
  await page.getByLabel("Card number").fill("4242424242424242")
  await page.getByLabel("Expiry").fill("12/28")
  await page.getByLabel("CVC").fill("123")
  await page.getByRole("button", { name: "Pay now" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible()
})

test("should handle declined card @regression", async ({ page }) => {
  await page.goto("/checkout")
  await page.getByLabel("Card number").fill("4000000000000002")
  await page.getByLabel("Expiry").fill("12/28")
  await page.getByLabel("CVC").fill("123")
  await page.getByRole("button", { name: "Pay now" }).click()
  await expect(page.getByRole("alert")).toContainText("Card declined")
})

test.describe("Payment Edge Cases @regression", () => {
  test("should handle network timeout during payment", async ({ page }) => {
    await page.goto("/checkout")
    await expect(page.getByText("Please try again")).toBeVisible()
  })
})

test("should render 3D Secure iframe @slow", async ({ page }) => {
  test.slow()
  await page.goto("/checkout/3ds")
  await expect(page.getByText("Verified")).toBeVisible()
})

test("should apply loyalty points discount", async ({ page }) => {
  test.skip(process.env.CI !== "true", "Only run in CI — needs loyalty service")
  await page.goto("/checkout")
})

test("should handle Apple Pay on Safari", async ({ page, browserName }) => {
  test.skip(browserName !== "webkit", "Apple Pay only works in Safari")
  await page.goto("/checkout")
})

test("should display PayPal button", async ({ page }) => {
  test.fixme() // Known broken — tracked in JIRA-1234
  await page.goto("/checkout")
  await expect(page.getByRole("button", { name: "PayPal" })).toBeVisible()
})

test("should fail when submitting empty card form", async ({ page }) => {
  test.fail()
  await page.goto("/checkout")
  await page.getByRole("button", { name: "Pay now" }).click()
  await expect(page.getByRole("alert")).toBeVisible()
})

注解速查表:

注解 效果 何时使用
test.skip() 完全跳过该测试 该环境/浏览器中功能不可用时
test.skip(condition, reason) 条件性跳过 浏览器特定或环境特定的测试
test.fixme() 跳过并标记为"需要修复" 已知错误,尚未修复
test.slow() 将超时时间延长至三倍 具有本质缓慢工作流的测试
test.fail() 预期测试失败;如果通过则视为失败 记录已知错误并附带回归保护
test.info().annotations 添加自定义注解 为报告添加自定义元数据

模式 5:测试筛选

何时使用:从 CLI 或 CI 运行测试子集。 何时避免:从不——需要了解这些命令。

# 按标签(在测试标题中)
npx playwright test --grep @smoke
npx playwright test --grep @regression
npx playwright test --grep-invert @slow     # 除 @slow 之外的所有测试

# 按文件
npx playwright test tests/auth/
npx playwright test tests/checkout/payment.spec.ts

# 按测试名称
npx playwright test --grep "should login"

# 按项目(来自 playwright.config
npx playwright test --project=chromium
npx playwright test --project=mobile-safari

# 组合筛选
npx playwright test --grep @smoke --project=chromium
npx playwright test tests/checkout/ --grep @smoke

# 按行号——运行单个测试
npx playwright test tests/auth/login.spec.ts:15

test.describe.configure() 中使用标签:

TypeScript:

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

export default defineConfig({
  projects: [
    {
      name: "smoke",
      testMatch: "**/*.spec.ts",
      grep: /@smoke/,
    },
    {
      name: "regression",
      testMatch: "**/*.spec.ts",
      grep: /@regression/,
    },
    {
      name: "all",
      testMatch: "**/*.spec.ts",
      grepInvert: /@slow/,
    },
  ],
})

JavaScript:

// playwright.config.js
const { defineConfig } = require("@playwright/test")

module.exports = defineConfig({
  projects: [
    {
      name: "smoke",
      testMatch: "**/*.spec.js",
      grep: /@smoke/,
    },
    {
      name: "regression",
      testMatch: "**/*.spec.js",
      grep: /@regression/,
    },
    {
      name: "all",
      testMatch: "**/*.spec.js",
      grepInvert: /@slow/,
    },
  ],
})

模式 6:并行与串行执行

何时使用:决定测试之间如何相对运行。 何时避免:测试数量 < 5(并行度无关紧要)。

默认:始终并行。 在配置中设置 fullyParallel: true

TypeScript:

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

export default defineConfig({
  fullyParallel: true, // 默认所有测试并行运行
  workers: process.env.CI ? 1 : undefined, // 本地使用全部核心,CI 使用 1 个(或自行调整)
})

JavaScript:

// playwright.config.js
const { defineConfig } = require("@playwright/test")

module.exports = defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 1 : undefined,
})

串行执行——仅当测试共享无法隔离的外部状态时使用:

TypeScript:

// tests/onboarding/wizard.spec.ts
import { test, expect } from "@playwright/test"

// 仅当各步骤确实依赖于无法独立设置的前序状态时,才使用 serial
test.describe.serial("Onboarding Wizard", () => {
  test("step 1: user enters company name", async ({ page }) => {
    await page.goto("/onboarding")
    await page.getByLabel("Company name").fill("Acme Corp")
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-2")
  })

  test("step 2: user selects plan", async ({ page }) => {
    await page.goto("/onboarding/step-2")
    await page.getByRole("radio", { name: "Pro plan" }).check()
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-3")
  })

  test("step 3: user confirms and completes", async ({ page }) => {
    await page.goto("/onboarding/step-3")
    await page.getByRole("button", { name: "Complete setup" }).click()
    await expect(page.getByText("Welcome to Acme Corp")).toBeVisible()
  })
})

JavaScript:

// tests/onboarding/wizard.spec.js
const { test, expect } = require("@playwright/test")

test.describe.serial("Onboarding Wizard", () => {
  test("step 1: user enters company name", async ({ page }) => {
    await page.goto("/onboarding")
    await page.getByLabel("Company name").fill("Acme Corp")
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-2")
  })

  test("step 2: user selects plan", async ({ page }) => {
    await page.goto("/onboarding/step-2")
    await page.getByRole("radio", { name: "Pro plan" }).check()
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-3")
  })

  test("step 3: user confirms and completes", async ({ page }) => {
    await page.goto("/onboarding/step-3")
    await page.getByRole("button", { name: "Complete setup" }).click()
    await expect(page.getByText("Welcome to Acme Corp")).toBeVisible()
  })
})

更好的替代方案:在一个测试中使用 test.step() 测试完整流程:

TypeScript:

// tests/onboarding/wizard.spec.ts
import { test, expect } from "@playwright/test"

test("user completes full onboarding wizard", async ({ page }) => {
  await test.step("enter company name", async () => {
    await page.goto("/onboarding")
    await page.getByLabel("Company name").fill("Acme Corp")
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-2")
  })

  await test.step("select plan", async () => {
    await page.getByRole("radio", { name: "Pro plan" }).check()
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-3")
  })

  await test.step("confirm and complete", async () => {
    await page.getByRole("button", { name: "Complete setup" }).click()
    await expect(page.getByText("Welcome to Acme Corp")).toBeVisible()
  })
})

JavaScript:

// tests/onboarding/wizard.spec.js
const { test, expect } = require("@playwright/test")

test("user completes full onboarding wizard", async ({ page }) => {
  await test.step("enter company name", async () => {
    await page.goto("/onboarding")
    await page.getByLabel("Company name").fill("Acme Corp")
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-2")
  })

  await test.step("select plan", async () => {
    await page.getByRole("radio", { name: "Pro plan" }).check()
    await page.getByRole("button", { name: "Next" }).click()
    await expect(page).toHaveURL("/onboarding/step-3")
  })

  await test.step("confirm and complete", async () => {
    await page.getByRole("button", { name: "Complete setup" }).click()
    await expect(page.getByText("Welcome to Acme Corp")).toBeVisible()
  })
})

模式 7Monorepo 测试

何时使用:单个仓库包含多个应用或包,每个都需要端到端测试。 何时避免:单应用仓库。

TypeScript:

// playwright.config.ts (monorepo 根目录)
import { defineConfig } from "@playwright/test"

export default defineConfig({
  projects: [
    {
      name: "web-app",
      testDir: "./apps/web/tests",
      use: {
        baseURL: "http://localhost:3000",
      },
    },
    {
      name: "admin-panel",
      testDir: "./apps/admin/tests",
      use: {
        baseURL: "http://localhost:3001",
      },
    },
    {
      name: "marketing-site",
      testDir: "./apps/marketing/tests",
      use: {
        baseURL: "http://localhost:4000",
      },
    },
  ],
})

JavaScript:

// playwright.config.js (monorepo 根目录)
const { defineConfig } = require("@playwright/test")

module.exports = defineConfig({
  projects: [
    {
      name: "web-app",
      testDir: "./apps/web/tests",
      use: {
        baseURL: "http://localhost:3000",
      },
    },
    {
      name: "admin-panel",
      testDir: "./apps/admin/tests",
      use: {
        baseURL: "http://localhost:3001",
      },
    },
    {
      name: "marketing-site",
      testDir: "./apps/marketing/tests",
      use: {
        baseURL: "http://localhost:4000",
      },
    },
  ],
})

Monorepo 目录布局:

monorepo/
├── apps/
│   ├── web/
│   │   ├── src/
│   │   └── tests/
│   │       ├── auth/
│   │       │   └── login.spec.ts
│   │       └── dashboard/
│   │           └── widgets.spec.ts
│   ├── admin/
│   │   ├── src/
│   │   └── tests/
│   │       └── user-management.spec.ts
│   └── marketing/
│       ├── src/
│       └── tests/
│           └── landing-page.spec.ts
├── packages/
│   └── shared-fixtures/
│       ├── auth.fixture.ts
│       └── index.ts
└── playwright.config.ts

仅运行一个应用的测试:

npx playwright test --project=web-app
npx playwright test --project=admin-panel

决策指南

你有多少个测试?
│
├── < 20 个测试(小型)
│   ├── 扁平结构:所有 .spec.ts 文件放在 tests/ 目录下
│   ├── 无需子目录
│   ├── 标签可选——直接全部运行
│   └── fullyParallel: true,单一配置项目
│
├── 20–200 个测试(中型)
│   ├── 特性子目录:tests/auth/、tests/checkout/
│   ├── 使用 @smoke 标签标记关键路径子集(< 15 分钟)
│   ├── 共享 fixtures 放在 tests/fixtures/ 下
│   ├── 页面对象放在 tests/page-objects/ 下(如使用)
│   └── 考虑为不同浏览器创建独立项目
│
└── 200+ 个测试(大型)
    ├── 顶层拆分:tests/e2e/、tests/api/、tests/visual/
    ├── 每个下面再按特性划分子目录
    ├── 使用 @smoke、@regression、@slow 标签——多条 CI 流水线
    ├── 在 CI 中使用分片(sharding)加快运行速度
    ├── 基于项目的筛选:smoke 项目、full 项目
    └── 将共享 fixtures 作为 monorepo 中的包管理
我该使用 test.describe.serial() 吗?
│
├── 每个测试能否通过 API/fixture 自行设置状态?
│   └── 是 → 不要使用 serial。使用独立的并行测试。
│
├── 这是一个多步骤的向导,每一步都会更改服务器状态,
│   且状态无法在测试之间重置?
│   └── 改为写成一个测试,内部使用 test.step()。
│
└── 这是真正有状态的场景(例如数据库迁移序列)?
    └── 作为最后手段使用 serial。添加注释说明原因。

反模式

一个巨型测试文件

// 不好:tests/all-tests.spec.ts 包含 80 个测试和 2000+ 行
test.describe("Everything", () => {
  test("login works", async ({ page }) => {
    /* ... */
  })
  test("signup works", async ({ page }) => {
    /* ... */
  })
  test("cart works", async ({ page }) => {
    /* ... */
  })
  // ... 还有 77 个测试
})

修复: 按特性拆分——每个特性区域一个文件,每文件 5–15 个测试。


无意义的测试名称

// 不好
test("test1", async ({ page }) => {
  /* ... */
})
test("test2", async ({ page }) => {
  /* ... */
})
test("payment test", async ({ page }) => {
  /* ... */
})
test("it works", async ({ page }) => {
  /* ... */
})

修复: 描述行为。当测试失败时,名称应该告诉你哪里出了问题。

// 好
test("should reject expired credit card", async ({ page }) => {
  /* ... */
})
test("user can update shipping address during checkout", async ({ page }) => {
  /* ... */
})

过深的 describe 嵌套

// 不好:3+ 层嵌套——难以阅读,在报告中难以找到测试
test.describe("Auth", () => {
  test.describe("Login", () => {
    test.describe("With MFA", () => {
      test.describe("SMS", () => {
        test("should send code", async ({ page }) => {
          /* ... */
        })
      })
    })
  })
})

修复: 最多 2 层。将更深层的嵌套拆分到单独的文件。

// 好:tests/auth/login-mfa-sms.spec.ts
test.describe("Login with SMS MFA", () => {
  test("should send verification code", async ({ page }) => {
    /* ... */
  })
  test("should reject invalid code", async ({ page }) => {
    /* ... */
  })
})

默认使用 test.describe.serial()

// 不好:无故使用 serial——扼杀并行性,产生隐藏依赖
test.describe.serial("User Profile", () => {
  test("should display profile page", async ({ page }) => {
    /* ... */
  })
  test("should update display name", async ({ page }) => {
    /* ... */
  })
  test("should upload avatar", async ({ page }) => {
    /* ... */
  })
})

修复: 每个测试应相互独立。使用 beforeEach 或 fixtures 来设置状态。

// 好
test.describe("User Profile", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/profile")
  })

  test("should display profile page", async ({ page }) => {
    /* ... */
  })
  test("should update display name", async ({ page }) => {
    /* ... */
  })
  test("should upload avatar", async ({ page }) => {
    /* ... */
  })
})

依赖测试执行顺序

// 不好:测试 2 依赖测试 1 创建数据
test("should create a product", async ({ page }) => {
  // 在数据库中创建"Widget X"
})

test("should edit the product", async ({ page }) => {
  // 假定"Widget X"已存在——单独或并行运行时出错
})

修复: 每个测试通过 API 调用或 fixtures 自行创建数据。

// 好
test("should edit a product", async ({ page, request }) => {
  // 独立设置测试数据
  const response = await request.post("/api/products", {
    data: { name: "Widget X", price: 9.99 },
  })
  const product = await response.json()

  await page.goto(`/products/${product.id}/edit`)
  await page.getByLabel("Name").fill("Widget X Updated")
  await page.getByRole("button", { name: "Save" }).click()
  await expect(page.getByText("Widget X Updated")).toBeVisible()
})

故障排查

问题 原因 修复方法
测试单独通过但一起失败 测试之间共享状态(cookie、数据库行、全局变量) 隔离每个测试——使用 fixtures 进行设置/清理
--grep @smoke 未匹配任何内容 标签未出现在测试标题字符串中 确认标签在 test('... @smoke', ...) 中字面出现
串行测试级联失败 一个测试失败,后续所有测试被跳过 重写为独立测试,或在一个测试中使用 test.step()
测试运行速度慢于预期 未设置 fullyParallel playwright.config 中添加 fullyParallel: true
CI 中运行了错误的测试 testMatchtestDir 配置错误 检查 playwright.config——使用 npx playwright test --list 打印已解析的配置
Monorepo 测试匹配到错误文件 默认 testDir. 为每个项目设置明确的 testDir

相关文档