Back to Blog返回博客
QA Playwright Email Testing Automation QA测试 Playwright 邮件测试 自动化

How to Test Email Verification in Playwright (2026 Complete Guide) 如何使用 Playwright 测试邮件验证码(2026 完整指南)

· 8 min read阅读约 8 分钟

End-to-end tests that involve email verification are notoriously fragile. You click "Register," an email gets sent, and now your test has to somehow read that email, extract a 6-digit code, and type it into a form — all in an automated, reproducible way. If you've tried to solve this before, you know the pain.

According to the 2024 World Quality Report by Capgemini, 41% of QA teams cite end-to-end test automation as their top challenge — with email verification flows ranked among the most frequently skipped tests in CI pipelines.

Source: Capgemini / Sogeti, World Quality Report 2024

Teams that skip email verification tests catch authentication regressions in production, where fixes cost 6× more.

The Problem with Traditional Approaches

Most teams encounter one of these dead ends when testing email flows:

  • Shared test accounts (Gmail): Works locally, breaks in CI because Google detects bot logins. You also get OTP codes mixed across parallel test runs.
  • Mailinator free plan: Public inboxes with no API access on the free tier. Any bot can read your test emails. Some services (including Amazon and Stripe) actively block Mailinator domains.
  • Self-hosted MailHog / MailDev: Requires SMTP configuration changes in your app, doesn't work for third-party auth flows (OAuth providers send real emails), and adds infra overhead.
  • Mocking the email entirely: You miss real template rendering bugs, broken links, and character encoding issues that only appear in actual emails.
  • Ephemeral email services: No stable API, rate-limited, and domains change unpredictably — a recipe for flaky tests.

What you actually need is a private, API-accessible mailbox that you own, can create programmatically, and can poll reliably in test code.

A 2023 Sauce Labs testing survey found that 62% of development teams experience at least one CI/CD pipeline failure per week caused by flaky tests — email-dependent tests being disproportionately represented.

Source: Sauce Labs, State of Digital Quality Report 2023

Section 1: Using a Real Mailbox API for Testing

The cleanest solution is to provision dedicated test mailboxes through an API before each test suite runs, use them during the test, and optionally clean them up afterward. GridInbox provides exactly this: a REST API to create mailboxes, list messages, and extract parsed OTP codes from incoming emails.

The key advantages over alternatives:

  • Each test gets its own isolated inbox — no cross-contamination between parallel runs
  • Inboxes are private (not publicly accessible like Mailinator)
  • OTP codes are parsed server-side and returned in a structured JSON field
  • Works with any email provider — the receiving infrastructure is real SMTP/SES
  • Custom domains are supported — use your own @test.yourdomain.com

Section 2: Complete Playwright Example

Let's walk through a full end-to-end test: register a new user, receive the verification email, extract the OTP, submit it, and assert the account is verified.

Step 1: Create a test inbox via the API

// helpers/email.ts
const API_BASE = 'https://api.gridinbox.com/api/v1';
const API_KEY  = process.env.GRIDINBOX_API_KEY!;

export async function createTestInbox(label: string) {
  const res = await fetch(`${API_BASE}/mailboxes`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({ name: label }),
  });
  const data = await res.json();
  // Returns { id, address, name, ... }
  return data as { id: string; address: string };
}

Step 2: Poll for the OTP email

export async function waitForOtp(
  mailboxId: string,
  options = { timeoutMs: 30_000, intervalMs: 2_000 }
): Promise<string> {
  const deadline = Date.now() + options.timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(`${API_BASE}/mailboxes/${mailboxId}/messages?limit=1`, {
      headers: { 'X-API-Key': API_KEY },
    });
    const { messages } = await res.json();

    if (messages?.length > 0) {
      const msg = messages[0];
      // GridInbox parses OTPs server-side
      if (msg.otp) return msg.otp as string;
      // Fallback: regex extraction from subject/text
      const match = (msg.subject + ' ' + msg.text_body).match(/\b(\d{4,8})\b/);
      if (match) return match[1];
    }

    await new Promise(r => setTimeout(r, options.intervalMs));
  }

  throw new Error(`OTP not received within ${options.timeoutMs}ms`);
}

Step 3: Full Playwright test

// tests/auth/email-verification.spec.ts
import { test, expect } from '@playwright/test';
import { createTestInbox, waitForOtp } from '../helpers/email';

test('user can verify email after registration', async ({ page }) => {
  // 1. Create a unique test inbox for this run
  const inbox = await createTestInbox(`pw-test-${Date.now()}`);
  const testEmail = inbox.address;

  // 2. Register with the test email
  await page.goto('/register');
  await page.fill('[name="email"]', testEmail);
  await page.fill('[name="password"]', 'TestPass123!');
  await page.click('[type="submit"]');

  // 3. Assert we're on the verification step
  await expect(page.locator('text=Check your email')).toBeVisible();

  // 4. Fetch the OTP from GridInbox API
  const otp = await waitForOtp(inbox.id);

  // 5. Fill in the OTP form
  await page.fill('[name="otp"]', otp);
  await page.click('[type="submit"]');

  // 6. Assert success
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('text=Welcome')).toBeVisible();
});

Section 3: Running in CI/CD

Add your API key as a repository secret in GitHub Actions and reference it in your workflow:

# .github/workflows/e2e.yml
name: E2E Tests

on: [push, pull_request]

jobs:
  playwright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run E2E tests
        env:
          GRIDINBOX_API_KEY: ${{ secrets.GRIDINBOX_API_KEY }}
        run: npx playwright test

Because each test creates its own isolated inbox, parallel test execution works perfectly. Playwright's built-in sharding (--shard=1/4) will distribute tests across runners without any inbox collisions.

Section 4: Best Practices

"The single most reliable pattern for email-dependent E2E tests is owning the inbox. Any solution that involves shared accounts, public disposable addresses, or mocking the SMTP layer will eventually create flaky tests that erode team confidence in the entire test suite."

Gleb Bahmutov, VP of Engineering, Cypress.io
  • Name inboxes semantically: Use a prefix like pw-{testName}-{timestamp} so you can identify which test a mailbox belongs to when debugging.
  • Set a polling timeout appropriate for your SES delivery speed: AWS SES typically delivers within 2–5 seconds. A 30-second timeout gives plenty of buffer. For slower SMTP relays, increase to 60 seconds.
  • Clean up after yourself: Add a test.afterAll hook that deletes test inboxes via the API. This keeps your GridInbox dashboard clean and avoids storage accumulation.
  • Use fixture-based inbox provisioning: Playwright fixtures let you share a single inbox across multiple tests in a file without re-creating it each time — a good pattern for "login once, test multiple pages" scenarios.
  • Don't hardcode email addresses: Always generate unique addresses per run. Hardcoded addresses lead to race conditions when tests run concurrently in a CI matrix.

Conclusion

Email verification testing doesn't have to be the flaky, manual-intervention-required nightmare that most teams experience. With a real mailbox API, you can make email flows a first-class part of your Playwright test suite — deterministic, parallel-safe, and CI-ready.

The pattern described here — create inbox → trigger email → poll API → extract OTP → assert — works for any email-based flow: verification, password reset, magic links, notification emails, and more.

涉及邮件验证的端到端测试向来是 QA 工程师的噩梦。用户点击"注册"后,系统发出一封邮件,而测试脚本必须以自动化、可重复的方式读取该邮件、提取 6 位验证码,并将其填入表单。如果你曾经尝试过解决这个问题,就一定深有体会。

传统方案的困境

大多数团队在测试邮件流程时会遭遇以下几个死胡同:

  • 共享测试账号(Gmail):本地可用,但在 CI 环境中会因 Google 检测到机器人登录而失败。多个并行测试还会产生 OTP 交叉污染。
  • Mailinator 免费版:公开收件箱且免费版无 API 接口。任何人都能读取你的测试邮件。部分服务(包括亚马逊和 Stripe)会主动屏蔽 Mailinator 域名。
  • 自托管 MailHog / MailDev:需要修改应用的 SMTP 配置,对第三方身份验证流程(OAuth 发送真实邮件)无效,还会带来额外的运维负担。
  • 完全 mock 邮件:会错过真实的模板渲染错误、失效链接和字符编码问题——这些 bug 只会在真实邮件中出现。
  • 一次性邮件服务:没有稳定 API、有速率限制、域名随时变化——是不稳定测试的温床。

你真正需要的是一个私有、可通过 API 访问的专属邮箱——可以通过代码创建、可靠轮询,且完全由你掌控。

第一节:使用真实邮箱 API 进行测试

最干净的解决方案是:在每个测试套件运行前通过 API 创建专用测试邮箱,测试期间使用,之后(可选)清理。GridInbox 正好提供了这套能力:REST API 支持创建邮箱、列出邮件、以及从来信中提取已解析的 OTP 验证码。

相比其他方案的核心优势:

  • 每个测试拥有独立收件箱,并行运行时零干扰
  • 收件箱是私有的,不像 Mailinator 那样公开可见
  • OTP 验证码由服务端解析,以结构化 JSON 字段返回
  • 兼容任何邮件提供商——底层接收基础设施为真实 SMTP/SES
  • 支持自定义域名,可使用 @test.yourdomain.com

第二节:完整 Playwright 示例

下面演示一个完整的端到端测试:注册新用户 → 接收验证邮件 → 提取 OTP → 提交验证 → 断言账号已激活。

第一步:通过 API 创建测试收件箱

// helpers/email.ts
const API_BASE = 'https://api.gridinbox.com/api/v1';
const API_KEY  = process.env.GRIDINBOX_API_KEY!;

export async function createTestInbox(label: string) {
  const res = await fetch(`${API_BASE}/mailboxes`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY, // 通过请求头传递 API 密钥
    },
    body: JSON.stringify({ name: label }),
  });
  const data = await res.json();
  // 返回 { id, address, name, ... }
  return data as { id: string; address: string };
}

第二步:轮询等待 OTP 邮件

export async function waitForOtp(
  mailboxId: string,
  options = { timeoutMs: 30_000, intervalMs: 2_000 }
): Promise<string> {
  const deadline = Date.now() + options.timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(`${API_BASE}/mailboxes/${mailboxId}/messages?limit=1`, {
      headers: { 'X-API-Key': API_KEY },
    });
    const { messages } = await res.json();

    if (messages?.length > 0) {
      const msg = messages[0];
      // GridInbox 服务端已解析 OTP
      if (msg.otp) return msg.otp as string;
      // 兜底:从主题/正文中用正则提取
      const match = (msg.subject + ' ' + msg.text_body).match(/\b(\d{4,8})\b/);
      if (match) return match[1];
    }

    // 等待后重试
    await new Promise(r => setTimeout(r, options.intervalMs));
  }

  throw new Error(`${options.timeoutMs}ms 内未收到 OTP,请检查邮件发送逻辑`);
}

第三步:完整 Playwright 测试用例

// tests/auth/email-verification.spec.ts
import { test, expect } from '@playwright/test';
import { createTestInbox, waitForOtp } from '../helpers/email';

test('用户注册后可完成邮件验证', async ({ page }) => {
  // 1. 为本次运行创建唯一测试收件箱
  const inbox = await createTestInbox(`pw-test-${Date.now()}`);
  const testEmail = inbox.address;

  // 2. 使用测试邮箱注册
  await page.goto('/register');
  await page.fill('[name="email"]', testEmail);
  await page.fill('[name="password"]', 'TestPass123!');
  await page.click('[type="submit"]');

  // 3. 断言已进入验证步骤
  await expect(page.locator('text=Check your email')).toBeVisible();

  // 4. 通过 GridInbox API 获取 OTP
  const otp = await waitForOtp(inbox.id);

  // 5. 填入验证码
  await page.fill('[name="otp"]', otp);
  await page.click('[type="submit"]');

  // 6. 断言跳转到 dashboard
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('text=Welcome')).toBeVisible();
});

第三节:在 CI/CD 中运行

将 API Key 作为 Repository Secret 添加到 GitHub Actions,并在 workflow 文件中引用:

# .github/workflows/e2e.yml
name: E2E 测试

on: [push, pull_request]

jobs:
  playwright:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: 运行端到端测试
        env:
          GRIDINBOX_API_KEY: ${{ secrets.GRIDINBOX_API_KEY }}
        run: npx playwright test

由于每个测试都创建了独立的收件箱,Playwright 的并行执行和分片功能(--shard=1/4)可以完美运行,不会出现任何收件箱冲突。

第四节:最佳实践

  • 语义化命名收件箱:使用 pw-{testName}-{timestamp} 格式前缀,方便在调试时识别收件箱归属。
  • 根据 SES 投递速度设置合理的轮询超时:AWS SES 通常在 2–5 秒内投递。30 秒超时足够充裕;对于较慢的 SMTP 中继,可适当增加至 60 秒。
  • 测试后清理收件箱:test.afterAll 钩子中调用 API 删除测试邮箱,保持 GridInbox 面板整洁,避免存储积累。
  • 使用 fixture 模式管理收件箱生命周期:Playwright fixture 可在一个测试文件的多个用例间共享同一收件箱,而无需每次重新创建——适合"一次登录、测试多个页面"的场景。
  • 永远不要硬编码邮件地址:始终为每次运行生成唯一地址。硬编码地址在 CI 矩阵并发时会产生竞态条件。

总结

邮件验证测试不必再是那个让团队头疼、需要人工干预的不稳定环节。借助真实邮箱 API,你可以让邮件流程成为 Playwright 测试套件中的一等公民——确定性、并行安全、CI 就绪。

本文描述的模式——创建收件箱 → 触发邮件 → 轮询 API → 提取 OTP → 断言结果——适用于所有基于邮件的流程:注册验证、密码重置、Magic Link 登录、通知邮件等。

Start Testing Email Flows Today 立即开始自动化邮件测试

Get a free GridInbox account and start creating isolated test inboxes via API in minutes. No credit card required. 免费注册 GridInbox,几分钟内即可通过 API 创建隔离测试收件箱。无需信用卡。

Create Free Account → 免费注册 →