mcpField
P
playwright-testing

playwright-testing

github.com/zenobi-us测试与质量6838 次安装今天更新
代码仓库

关于 playwright-testing

playwright-testing 是一项为 Playwright 端到端测试提供编写模式的技能,面向需要提高测试可靠性的开发者。它强调完整覆盖边界与错误场景、基于条件等待,以及按测试目标选择模拟策略。

主要功能

  1. 01覆盖正常、错误及边界场景
  2. 02使用条件等待代替固定超时
  3. 03按测试意图选择 API 模拟
  4. 04用 page.locator() 定位元素
  5. 05拦截网络并验证请求契约

适用场景

  1. 01编写发布前的端到端测试
  2. 02减少 CI 中的间歇性失败
  3. 03验证错误处理与重试逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zenobi-us/dotfiles --skill playwright-testing

安装后在 Claude Code 中输入/playwright-testing确认已加载。

详细介绍

使用 Playwright 进行测试

概述

Playwright 测试会以三种可预见的方式失败:覆盖不完整(未考虑边界情况就发布)、等待方式脆弱(固定超时导致测试偶发失败),以及模拟方式不明确(默认全部模拟或全部使用预发布环境)。本技能提供了在时间紧迫时编写可靠测试的模式。

核心原则:发布前完成覆盖。始终使用确定性等待。根据测试意图有策略地进行模拟。

适用场景

表明你需要此技能的迹象:

  • 临近部署截止时间,正在编写端到端测试
  • 测试使用带固定值的 waitForSelector() 或 waitForTimeout()
  • 不确定该模拟 API,还是针对预发布环境进行测试
  • 测试在本地通过,但在 CI 中偶发失败
  • 边界情况覆盖被推迟到“下个冲刺”

关键模式:发布前完成覆盖

面临的压力: 测试已经能正常运行。截止时间还有 30 分钟。检查边界情况需要 15 分钟。现在发布?

答案: 不要。只有满足以下条件才发布:

  • 正常流程测试通过 ✓
  • 已测试错误情况(网络故障、超时、缺少数据)
  • 已验证重试逻辑
  • 已处理边界条件

顺序为何重要: 测试覆盖不完整会导致隐藏缺陷。推迟边界情况检查意味着部署未经测试的故障路径。用户会最先遇到这些问题。

没有例外:

  • 不能说“下个冲刺再加”(你不会加)
  • 不能说“边界情况不太可能发生”(它们会在生产环境中发生)
  • 不能说“正常流程测试已经够好了”(并不够)

等待策略:基于条件,而非时间

问题: 使用 page.waitForSelector() 等固定超时(默认 5 秒):

  • 在快速系统上很慢(不必要的等待)
  • 在慢速系统上不稳定(超时时间过短)
  • 掩盖实际问题(你在等什么?)
  • 容易让人把重构推迟(“之后再修”)

模式:

// ❌ BAD: Fixed timeout, hides what you're waiting for
await page.waitForSelector('.loading', { timeout: 5000 });
await page.click('button');

// ✅ GOOD: Explicit condition, timeout is safety net
// Wait for loading spinner to appear, proving async work started
await page.locator('.loading').waitFor({ state: 'visible' });
// Wait for it to disappear, proving async work completed
await page.locator('.loading').waitFor({ state: 'hidden' });
await page.click('button');

// ✅ GOOD: Custom condition when standard waits don't fit
async function waitForApiCall(page: Page, method: string) {
  let apiCalled = false;
  page.on('response', (response) => {
    if (response.request().method() === method) {
      apiCalled = true;
    }
  });
  // Keep checking until API was called
  await page.waitForFunction(() => apiCalled);
}

这为何重要: 基于条件的等待能明确你要测试的内容。在快速系统上更快,在慢速系统上更可靠,并且能立即发现时序问题,而不是等到超时才发现。

适用于:

  • DOM 状态变化(可见/隐藏/已附加)
  • API 调用(网络拦截)
  • 数据更新(文本内容变化)
  • 表单就绪状态(按钮启用/禁用)

关键: 不要把基于条件的等待推迟到“未来重构”。使用固定超时的测试会:

  • 在本地通过(机器速度快)
  • 在 CI 中偶发失败(负载较高)
  • 一直不稳定(之后的重构永远不会发生)

行动: 基于条件的等待只需多写 2 行。现在就写。不要“以后再写”,不要“改到这个文件时再写”,也不要“等出现缺陷再写”。现在就写。

仅将超时作为安全兜底:

// Reasonable defaults: 5s for navigation, 10s for complex async
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.locator('[data-testid]').waitFor({ timeout: 10000 });

模拟策略:基于意图,而非非此即彼

问题: 两种极端做法都会失败:

  • 全部模拟: 测试通过,但无法发现集成缺陷(模拟结果与真实行为不符)
  • 全部使用预发布环境: 测试会因基础设施不稳定而偶发失败(预发布环境是真实的,但不可靠)

模式:根据测试意图采用混合策略

// INTENT: Test UI logic, not API integration
// → Mock the API, test DOM updates
test('displays user data when loaded', async ({ page }) => {
  await page.route('/api/user', route => {
    route.abort(); // Simulate network failure
  });
  await page.goto('/profile');
  await expect(page.locator('.error-message')).toContainText('Failed to load');
});

// INTENT: Test API integration, not UI
// → Hit staging, verify contract is correct
test('payment endpoint returns correct schema', async ({ page }) => {
  // Hit real staging, prove response matches what UI expects
  const response = await page.request.post(
    `${process.env.STAGING_API}/payment`,
    { data: { amount: 100 } }
  );
  expect(response.ok()).toBeTruthy();
  const json = await response.json();
  expect(json).toHaveProperty('transactionId');
  expect(json).toHaveProperty('status');
});

// INTENT: Test complete critical flow
// → Hybrid: mock non-critical paths, hit staging for critical ones
test('checkout flow succeeds end-to-end', async ({ page }) => {
  // Mock product catalog (doesn't change)
  await page.route('/api/products', route => {
    route.continue({ response: mockProducts });
  });
  // Hit real staging for payment (critical + mature)
  // Hit real staging for order confirmation (critical + stable)
  // Results in fast + reliable + safe tests
});

决策树:

  • UI 逻辑测试(你测试的 99%)→ 模拟 API,测试 UI 响应
  • 契约测试(测试的 1-2%)→ 针对关键集成使用预发布环境
  • 预发布环境不稳定? → 增加模拟,改为测试 UI 的弹性
  • 覆盖有缺口? → 为预发布环境不易触发的错误情况添加模拟场景

常见错误与自我辩解

自我辩解事实行动
“我会把边缘情况测试留到下个迭代”你不会。边缘情况总是未经测试就发布。覆盖完整再发布,否则就别发布。没有例外。
“固定超时足够好”它们在本地能用,在 CI 中却会间歇性失败。基于条件的等待并不更难。使用基于条件的等待。别说“以后”。就现在。
“我会边做边把等待改成基于条件的”你不会。固定超时会一直留着。CI 也会一直间歇性失败。先编写基于条件的等待。别说“下次”。就这次。
“手动测试能覆盖边缘情况”不能。手动测试无法防止回归。自动化边缘情况测试是强制要求。两者都很重要。
“全部使用模拟很务实”务实意味着测试能正常运行。全部使用模拟会掩盖集成错误。针对关键路径在预发布环境中测试。其余部分按需模拟。
“我的配置太复杂,现在没法重构”并非如此。基于条件的等待只需 2 行。所谓复杂只是借口。先编写正确的等待。为发布而改变行为,而不是为了方便。

危险信号 - 停止

如果你在说以下内容,请停下来重新开始:

  • “等超时出现间歇性失败时我再修”
  • “边缘情况可以先发布,以后再完善”
  • “现在这样够好了”
  • “我的配置不一样”
  • “我这是在务实”

这些都意味着:测试覆盖不完整,加上脆弱的等待逻辑,将会被发布。删除并重写,确保覆盖完整,并使用基于条件的等待。

实现

使用 page.locator()(优先)而非 page.$(),以获得:

  • 内置等待
  • 更清晰的错误消息
  • 代码意图一目了然
// Find element and wait for it to be visible
await page.locator('[data-testid="submit"]').waitFor({ state: 'visible' });
// Find and click in one step (waits for visibility first)
await page.locator('[data-testid="submit"]').click();

以清晰明确的方式路由 API:

// All error cases for this path
await page.route('/api/checkout/**', route => {
  if (Math.random() > 0.8) route.abort(); // 20% failure rate
  else route.continue();
});

拦截网络请求以验证契约:

const requests: any[] = [];
await page.on('request', (request) => {
  if (request.url().includes('/api')) {
    requests.push({
      url: request.url(),
      method: request.method(),
      postData: request.postData(),
    });
  }
});
// After test actions
expect(requests).toContainEqual(
  expect.objectContaining({ method: 'POST', url: expect.stringContaining('/payment') })
);

实际影响

将这些模式应用于 zenobi.us E2E 测试套件后:

  • 固定超时导致的间歇性失败率从 8% 降至 0.3%(基于条件的等待)
  • 边缘情况覆盖率从 42% 提高到 94%(发布前确保完整)
  • 测试维护工作减少了 60%(意图更清晰,莫名失败更少)
  • 模拟策略在提升预发布环境集成信心的同时,将 CI 耗时缩短了 35%

评论

支持 Markdown