mcpField
E
email-testing

email-testing

github.com/petrkindlmann测试与质量2221.0k 次安装6 天前更新
代码仓库

关于 email-testing

用于注册确认、密码重置、魔法链接和 OTP 等邮件流程的端到端测试技能。它指导团队捕获真实邮件、可靠轮询收件箱、验证邮件内容,并将投递能力检查与功能测试分开。

主要功能

  1. 01捕获真实测试邮件
  2. 02轮询收件箱而不固定等待
  3. 03提取 OTP 和经批准的链接
  4. 04按收件人隔离并行测试
  5. 05断言主题、发件人和链接
  6. 06分离邮件投递检查

适用场景

  1. 01从收件箱到完成地测试注册确认
  2. 02测试密码重置和魔法链接流程
  3. 03验证 OTP 与 MFA 邮件流程
  4. 04单独运行 SPF、DKIM 和 DMARC 检查

安装

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

$npx skills add https://github.com/petrkindlmann/qa-skills --skill email-testing

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
deliverability.md
hosted-inboxes.md
mailpit-playwright.md

详细介绍

依赖电子邮件的流程可能会静默失败:通过直接调用确认端点来测试“注册并点击确认”,永远无法证明邮件确实已生成、发送给正确收件人、应用了模板并包含可用链接。此技能会捕获真实邮件,无需固定等待时长即可等待邮件到达,从正文中提取唯一明确的 OTP 或链接,并完成整个流程——这样,模板损坏、令牌未经签名或收件人错误等问题都会导致测试失败。 它还会将送达能力(SPF/DKIM/DMARC)检查放在单独的非阻塞测试套件中,这样 DNS 问题就不会导致功能测试门禁失败。

快速导航

情况前往
选择邮件捕获工具下方的“收件箱捕获决策树”
无需等待固定时长即可轮询收件箱references/mailpit-playwright.md(轮询辅助工具)
从正文中提取 OTP / 链接references/mailpit-playwright.md(提取)
完整的密码重置 / 注册 / 魔法链接端到端测试references/mailpit-playwright.md
在预发布环境使用真实地址references/hosted-inboxes.md(Mailosaur)
为每项测试使用一次性收件箱references/hosted-inboxes.md(MailSlurp / ShazaMail)
仅在本地预览模板references/hosted-inboxes.md(Ethereal)
标记 SPF/DKIM/DMARC 问题references/deliverability.md
测试在本地通过,但在 CI 中提示“尚无邮件”下方的“CI 中的邮件间歇性故障”

发现问题

先检查 .agents/qa-project-context.md——如果该文件存在,请使用其中的信息,并跳过已回答的问题。

  • 需要在哪里接收邮件? 对于大多数功能流程,使用本地捕获工具(Mailpit)即可,而且免费。如果需要真实且可对外送达的地址(预发布环境、第三方 ESP、真实 DNS),则应使用托管收件箱(Mailosaur / MailSlurp)。这是选择工具时最重要的因素。
  • 测试套件的并行度如何? 高并行度会让“最新邮件”变得含糊——需要为每项测试使用唯一地址或独立收件箱,而不是共用邮箱。
  • 涉及哪些流程? 注册确认、密码重置、魔法链接登录、OTP/MFA、通知邮件——这些流程遵循相同的模式(捕获、提取、完成),但提取的内容不同(链接或 6 位数字代码)。
  • 是否需要检查送达能力? “邮件到达收件箱 / SPF、DKIM、DMARC 检查通过”属于单独的非阻塞测试套件,不应纳入功能性 OTP 测试。请提前确定。
  • 仅在本地预览,还是要在 CI 中断言? 开发时预览模板可使用 Ethereal;在 CI 中进行断言则应使用 Mailpit/Mailosaur/MailSlurp/ShazaMail。不要混淆两者。

核心原则

  1. 捕获真实邮件;绝不走捷径跳过它。 直接调用重置端点或硬编码令牌,都会跳过这项测试要验证的完整集成流程——模板渲染、收件人解析、链接生成、令牌签名。在界面上提交,然后读取收件箱。

  2. 轮询,绝不固定等待。 邮件到达是异步的。固定调用 waitForTimeout 要么太短(导致测试不稳定),要么太慢(浪费数分钟),这是邮件测试不稳定的首要原因。使用针对 /api/v1/search?query=... 的 expect.poll / .toPass,或使用内置等待器(messages.get、waitForLatestEmail),它们已经会替你轮询。在客户端限制每次 HTTP 调用的时长,包括解析响应正文;仅设置提供方的等待时限,无法防止连接卡住。

  3. 每个测试使用一个唯一收件人。 两个并行测试如果都读取“最新的注册邮件”,就会拿到对方的邮件。确保每个测试的地址都唯一(使用加号寻址或每个测试单独的收件箱),并且读取时按收件人筛选。ShazaMail 也可以通过专用测试收件箱 ID 和 received_after 匹配器隔离每个测试。在测试之间清空收件箱无法解决并行测试的问题。

  4. 根据真实模板提取内容,并排除歧义。 使用针对标签的正则表达式从邮件正文中提取 OTP,并要求只有一个不同的验证码。使用 DOM 解析器解析 HTML 链接,然后要求链接的来源完全匹配获准的来源,并且路径和令牌符合当前流程。未匹配到或匹配结果存在歧义时,测试必须失败;绝不跳转到猜测的 URL。

  5. 断言内容,而非是否存在。 “收到了一封邮件”是很弱的断言。检查 subject、from 地址,以及链接是否指向正确的域名。只有对内容进行断言,才能发现模板错误和链接错误。

  6. 可送达性测试应独立运行,不阻塞其他测试。 SPF/DKIM/DMARC 的结果来自真实收件服务器的身份验证信息,绝不能从正文文本中判断,而且不得阻塞功能流程测试。


收件箱捕获决策树

选择能够实际接收邮件的最低成本工具。默认使用 Mailpit 进行本地和 CI 功能测试;只有需要真实地址或高并行隔离时,才选择托管收件箱。

工具托管方式 / 成本地址类型适用场景
Mailpit自托管,单个二进制文件或 docker,免费 / 开源任意本地 SMTP 收件人默认选择。本地和 GitHub Actions 功能测试,预算紧张时使用。REST API 位于 :8025(/api/v1/messages)。
Mailosaur托管(API key),付费真实 *.mailosaur.net 地址需要真实可投递地址的类预发布 / 生产流程;支持自动等待 messages.get(serverId, { sentTo });提供结构化链接 / 验证码;使用真实 SPF/DKIM/DMARC。
MailSlurp托管(API key),付费真实地址,每个收件箱一个通过 createInbox() + waitForLatestEmail 为每项测试创建一次性收件箱;并行隔离能力强。
ShazaMail托管(Bearer API key),仅接收邮件真实地址,每项测试一个收件箱通过 POST /api/v1/test-inboxes + 有时限的 POST /api/v1/test-inboxes/:id/wait 测试注册 / 重置 / OTP 流程;不提供出站 SMTP。
Ethereal托管式临时服务,免费可捕获邮件,不会实际投递仅用于本地开发中的模板预览(createTestAccount + getTestMessageUrl)。不适用于 CI 断言。

对于预算有限的注册流程测试(本地 + GitHub Actions,可接受自托管):使用 Mailpit——一个二进制文件 / docker 镜像,并提供位于 :8025 的免费开源 REST API。如果之后需要真实可投递地址,可将该测试套件升级为 Mailosaur、MailSlurp 或 ShazaMail(提供真实地址的托管收件箱)。不要“直接检查数据库而不检查邮件”——这只能证明写入了数据行,不能证明邮件已发送、地址正确且链接可用。

避免使用:MailHog——自 2020 年起已归档 / 无人维护;Mailpit 是它的即插即用替代品(端口相同,API 兼容),截至 2026 年中已验证。新测试套件也应跳过 smtp4dev / Papercut——Mailpit 的 API 和全文搜索更适合自动化断言。

参见 references/mailpit-playwright.md,了解 docker-compose、轮询辅助函数和提取工具;参见 references/hosted-inboxes.md,了解 Mailosaur / MailSlurp / Ethereal。


轮询收件箱(Mailpit)

使用 Playwright 的 request fixture 读取搜索端点,找到发给此测试收件人的邮件,然后按 ID 获取完整正文。使用带有 timeout 和 intervals 的 expect.poll——它会不断重试,直到出现匹配项,因此响应迅速的收件箱会立即返回,较慢的收件箱也能通过测试。

// `request` is the Playwright APIRequestContext fixture; plain fetch() works too.
await expect.poll(async () => {
  const query = new URLSearchParams({ query: `to:${to}` });
  const res = await request.get(`http://localhost:8025/api/v1/search?${query}`);
  expect(res.ok(), `Mailpit search HTTP ${res.status()}`).toBe(true);
  const { messages } = await res.json();
  return messages.find((m) => m.To.some((t) => t.Address === to))?.ID ?? null;
}, { timeout: 30_000, intervals: [500, 1_000, 2_000] }).not.toBeNull();
// then: request.get(`http://localhost:8025/api/v1/message/${id}`) → { Text, HTML, Subject, From, To }

搜索端点的 query=to: 筛选条件与针对收件人的 .find,是并行测试能够保持确定性的关键。绝不要在没有收件人筛选条件时使用 messages[0] / messages.at(-1)(全局最新邮件)。如果要向同一账户请求第二封邮件,请排除已见过的 ID;已有的匹配邮件不能满足新的请求。完整辅助函数见 references/mailpit-playwright.md,其中包括 HTTP / schema 错误处理和详情检查。


提取 OTP 和链接

使用 references/mailpit-playwright.md 中经过防护的辅助函数处理邮件正文:

const otp = extractOtp(message.Text, /\bcode:\s*(\d{6})\b/gi);
const resetUrl = await extractLink(page, message, new URL(baseURL).origin,
  url => url.pathname === '/reset' && Boolean(url.searchParams.get('token')));
await page.goto(resetUrl);

不要按索引截取(body.split(' ')[3]、substring(0, 6)、indexOf('code'))——模板一旦改动一个词,这些方法就会失效。需要读取邮件正文时,不要读取实时页面的 innerText。如果 6 位验证码可能与其他数字冲突,请以标签为锚点,并拒绝多个不同的匹配结果。宽泛的 \S* URL 正则可能会吞掉闭合 HTML 标签,并保留查询参数中的 &。托管服务会提供结构化的 message.html.links / message.html.codes——如可用,应优先使用。参见 references/mailpit-playwright.md。


确定性地址(并行隔离)

并行测试中的问题:两个测试同时注册,并且都轮询“最新的注册邮件”,结果彼此拿到了对方的邮件。修复方法按优先级排序:

  • 每个测试使用唯一地址 — 使用加号寻址/子地址: user+${randomUUID()}@example.test,每次重试都重新创建。仅靠时间戳可能会在不同工作进程之间发生冲突。大多数服务提供商会将 user+anything@ 路由到 user@,因此一个真实邮箱就能生成无限个唯一收件人。
  • 每次读取都按收件人筛选 — 使用 sentTo(Mailosaur),或使用 query=to: + .find 匹配(Mailpit)。绝不要直接获取最新的一封邮件。
  • 每个测试使用专用收件箱 — MailSlurp 的 createInbox() 或 ShazaMail 的 POST /api/v1/test-inboxes 会为每个测试提供独立收件箱;Mailosaur 会为每个测试在你的服务器域名下提供唯一地址。

在测试之间清空收件箱,在并行执行时并不足够——两个同时运行的测试仍会发生冲突。唯一地址 + 收件人筛选才是真正的解决方案。参见 references/hosted-inboxes.md。


断言主题/发件人/标头/链接

捕获邮件后,对内容进行断言:

  • expect(message.subject).toBe('Welcome to Example') — 可发现模板错误。
  • expect(message.from?.[0].email).toBe('[email protected]') — 可发现发件人/回复地址配置错误。
  • 如果产品设置了标头(List-Unsubscribe、自定义 X- 标头),也要进行检查。
  • 链接应指向正确的域名: 在导航前使用 expect(new URL(link.href).origin).toBe('https://staging.example.com')。 检查主机名后缀也会接受 evil-staging.example.com;应要求来源完全匹配,或明确获准的子域边界。

有关断言 subject、from 和链接域名的 Mailosaur 示例,请参见 references/hosted-inboxes.md。


可投递性:SPF/DKIM/DMARC

将其放在独立的非阻塞测试套件中,与功能流程测试分开。SPF、 DKIM 和 DMARC 的 pass/fail + 对齐状态来自真实收件服务器对你发送域名的验证——它们不是正文中的字符串,因此绝不要使用 body.includes('spf') 或 body.match(/dkim/)。并且,Mailpit 不会验证 SPF/DKIM/DMARC 对齐——它只进行基本的 SpamAssassin 内容评分,因为邮件并未通过真实 DNS 发送。验证真实对齐需要托管式发送和接收 (Mailosaur 的可投递性报告,或用于临时检查的 mail-tester.com)。为测试套件添加 @deliverability 标记,将其设为非必需 CI 任务(continue-on-error),并且绝不要在 OTP/重置流程测试中断言可投递性。参见 references/deliverability.md。


CI 中的邮件不稳定问题

测试在本地通过,但 CI 断言时邮件“还没到”。根本原因有四种——全部排查,不要只增加等待时间:

  1. 使用固定等待时间,而不是轮询。 waitForTimeout/任意延迟会与邮件到达时间发生竞争。改用 expect.poll/.toPass/内置的 waitFor。
  2. 超时时间太短。 CI 中的邮件投递比本地慢。使用轮询后,应增加轮询的 timeout(例如 30–60s),而不是盲目延长等待时间。
  3. 没有按收件人筛选。 并行执行时,读取最新邮件可能会拿到另一个测试的邮件。使用 sentTo/to: 进行筛选,并为每个测试使用唯一地址。
  4. 存在上一次运行留下的旧邮件。 旧的匹配邮件会在新邮件到达前满足断言。在全局设置中清空收件箱,和/或每次运行使用唯一地址,确保上一次运行的邮件不会匹配。

重试整个任务、隔离该测试,或将全局等待时间提高到 30s,都只是在处理症状,无法消除竞争条件。


反模式

1. 读取收件箱前固定等待

先执行 page.waitForTimeout(5000)/setTimeout/sleep() 再读取。等待太短会导致测试不稳定,太长则会浪费几分钟。应使用 expect.poll 轮询 /api/v1/messages(或使用内置等待器)。

2. 推荐已经停用的邮件捕获工具

MailHog 已归档(2020)。Papercut/smtp4dev 在自动化方面较弱。请使用 Mailpit。

3. 检查数据库,而不是邮件

数据库中的一行记录只能证明写入已发生,不能证明邮件已发送、收件人正确且链接可用。 读取实际捕获的邮件。

4. 绕过邮件直接走捷径

直接调用重置端点或硬编码令牌,会跳过模板、链接生成和令牌签名。捕获邮件,提取链接,然后使用 page.goto 打开。

5. 使用脆弱的基于索引的提取方式

body.split(' ')[3]、substring(0, 6)、indexOf('code')。请使用带空值保护的锚定 \d{6} 正则表达式。并且要从邮件正文提取,而不是从当前页面的 innerText 提取。

6. 不筛选收件人,直接取最新邮件

messages[0] / messages.at(-1) 在并行运行时会发生冲突。按收件人筛选,并为每个测试使用唯一地址;仅在测试之间“删除所有邮件”并不能解决问题。

7. 只断言邮件存在

不检查 subject / from / 链接,就会漏掉模板错误和链接错误。请断言邮件内容。

8. 从正文中用正则提取 SPF/DKIM/DMARC,或对此依赖 Mailpit

认证结果来自真实的收件服务器,而不是正文文本;Mailpit 只进行垃圾邮件评分。请使用 Mailosaur 的投递能力报告,并将其放在单独的非阻塞套件中。

9. 使用 IMAP 库连接真实邮箱

imap-simple / node-imap / imapflow 会重复实现轮询,还会失去隔离性。请使用 MailSlurp 的 createInbox + waitForLatestEmail,每个测试使用一个收件箱。

10. 在 CI 中使用 Ethereal,或在本地预览时使用付费服务

Ethereal 不会投递任何邮件——它仅用于预览(createTestAccount + getTestMessageUrl)。不要在 CI 中对它进行断言;同样,也不要仅仅为了在本地查看模板,就启动付费托管服务。


验证

证明套件确实捕获并断言了邮件,从最小检查开始:

  • 已接入捕获: curl -s localhost:8025/api/v1/messages | jq '.total' 返回一个数字(Mailpit 已启动,API 可访问)。对于托管收件箱,一行 messages.get / createInbox 冒烟脚本应能在没有认证错误的情况下返回。
  • 没有盲目等待: 对邮件规格运行 grep -rE 'waitForTimeout|sleep\(|setTimeout' tests/,不应打印任何内容。
  • 流程通过且真实: 运行注册/重置规格,并确认临时破坏模板主题后测试会失败——如果仍然通过,说明你没有断言内容。
  • 确定性成立: 使用 --workers=4 --repeat-each=3 运行邮件规格;通过即证明按收件人筛选在并行运行时仍然有效。
  • 投递能力测试已隔离: --grep @deliverability 仅选择认证套件,并且该 CI 任务设置为 continue-on-error / 非必需。

完成条件

  • 根据决策树选择捕获工具,并记录在 .agents/qa-project-context.md 中(本地/CI 使用 Mailpit,只有需要真实地址时才使用托管收件箱)。
  • 在读取收件箱前,任何邮件测试都不包含 waitForTimeout / sleep / setTimeout——对邮件规格运行 grep -rE 'waitForTimeout|sleep\(|setTimeout' tests/ 应无结果;通过 expect.poll / .toPass / 内置等待器等待邮件到达。
  • 每个邮件测试都使用唯一收件人(加号地址或每个测试单独的收件箱),并按该收件人筛选读取结果——不能在未筛选的情况下使用 messages[0] / messages.at(-1)。
  • OTP 提取使用模板标签,并拒绝互不相同的竞争验证码;链接提取解析 HTML 锚点,并要求与确切的获准来源、路径和令牌相匹配的唯一结果。缺失或含糊的结果必须在导航前导致失败。
  • 至少有一个注册确认(或重置 / 魔法链接)流程具备通过的 E2E 测试,该测试会捕获真实邮件,并通过 page.goto(link) 完成流程。
  • 存在对 subject、from 和链接域名的内容断言,而不只是断言邮件存在。
  • 如果范围包含投递能力(SPF/DKIM/DMARC)测试,这些测试应放在单独的 @deliverability 套件中,并且在 CI 中不阻塞。

相关技能

  • playwright-automation — 每个邮件流程中负责驱动浏览器的部分:表单、 导航、测试夹具和轮询辅助工具。此技能补充收件箱相关功能。
  • api-testing — 如需直接测试邮件服务商的 API,或测试应用是否会发送邮件,请参阅此技能;本技能专注于在端到端流程中接收邮件并进行断言。
  • test-data-management — 生成每项测试专属的唯一地址、工厂和种子用户,为此处的收件人策略提供数据。
  • qa-project-context — 记录所选的捕获工具、SMTP 目标和凭据,确保所有邮件测试共用一套配置。

参考文件(位于 references/ 中)

  • mailpit-playwright.md — docker-compose、expect.poll Mailpit 辅助工具、OTP/链接提取工具,以及完整的密码重置/注册/OTP/魔法链接端到端测试。
  • hosted-inboxes.md — Mailosaur(messages.get 自动等待、真实地址、结构化链接/代码)、MailSlurp(createInbox + waitForLatestEmail,每项测试独立收件箱)、ShazaMail(每项测试独立收件箱,并为创建/等待设置客户端超时),以及 Ethereal(通过 createTestAccount + getTestMessageUrl 在本地预览)。
  • deliverability.md — 将 SPF/DKIM/DMARC 作为独立的非阻塞测试套件,说明为何正文正则表达式和 Mailpit 无法验证身份验证,以及 Mailosaur 的可送达性断言。

评论

支持 Markdown