验证码偶尔被另一条并行用例读走,或重试后拿到旧邮件?
最快处理:给争用同一测试账号或收件资源的用例声明相同的 Playwright 命名锁;再用唯一测试标识或隔离收件目标确认邮件归属。锁能协调共享资源访问,但不会单独防止读错验证码邮件。
本周建议动作:先复现并记录共享资源冲突,再给真正争用资源的用例加锁;如果邮件仍无法对应到当前运行,就改造邮件关联断言或隔离收件目标。本文依据 Playwright 官方发布说明、并行测试指南与 API 文档核对,最后更新于 2026 年 10 月 24 日。
这篇适合跨文件、worker 或 project 并行运行测试的开发者,以及负责注册、验证码邮件 E2E 和 CI 测试环境的 QA 与工程师。
如果你只是在测试第三方平台,或没有授权读取其邮件,不要照此操作;以下步骤仅面向你自己的产品或获授权环境。
准备阶段:先定位争用的邮件测试资源
先画出一次用例的收发路径:测试触发注册或验证码请求后,邮件写入哪里,由哪个读取接口取回,测试账号和收件地址是否会被其他用例复用。把“偶发失败”直接归咎于并行,会漏掉发送失败、延迟到达、筛选条件过宽或旧邮件未清理等原因。
Playwright 的 worker 会各自运行测试,浏览器上下文与内存状态可以隔离;但共享测试账号、后端记录、外部收件箱或邮件读取接口仍可能在用例之间形成竞争。官方建议优先隔离并行测试依赖的状态;锁适用于不支持并发访问的共享资源。(Playwright 并行测试指南)
先把故障分成两类:
- 资源互斥问题:两条测试同时修改同一账号、清理同一收件箱,或争用同一读取接口。可考虑加锁。
- 邮件关联问题:测试虽然没有并发操作,仍可能读到旧邮件,或无法判断邮件属于哪次运行。加锁不能替代关联标识和邮件断言。
- 发送链路问题:邮件根本没发出,或抵达时间超出测试等待范围。锁不负责改善邮件送达。
如果还没明确验证哪一种,就先保留失败日志、收件箱筛选条件和测试运行标识,再分别复现;不要一开始就把整个邮件测试套件串行化。
配置阶段:按共享资源范围定义命名锁
Playwright 测试锁邮件测试的关键不是给所有用例贴同一个锁,而是让争用同一资源的用例使用同一锁名。共享账号可以使用 account:signup-qa,共享收件箱可以使用 inbox:signup-qa;独立资源则不要为了方便共用这两个名字。
Playwright 官方发布说明记录了测试锁功能,并说明共享锁名的测试不会并发运行,跨文件、worker 与 project 也适用。具体语法和可用范围要以项目安装的 Playwright 版本及对应官方 API 文档为准;发布说明记录该功能是在 1.63 版本加入。你可以先运行 npx playwright --version,再对照测试锁发布说明和Playwright Test API 文档检查。
单个测试可这样声明:
import { test, expect } from '@playwright/test';
test('注册邮件验证码可用', {
lock: 'inbox:signup-qa',
}, async ({ page }) => {
// 只测试自家产品或获授权环境
});
需要让一组相关用例共用锁时,可使用 test.describe() 的组级配置:
test.describe('共享注册收件资源', {
lock: 'inbox:signup-qa',
}, () => {
test('发送验证码', async ({ page }) => {
// ...
});
test('验证注册结果', async ({ page }) => {
// ...
});
});
如果你看到的是 test.describe.configure() 写法,不要假设它和其他配置形式在所有版本中都相同;先核对当前版本的 API 说明。官方文档也提示,在默认或串行模式下,同一文件内只要某个测试声明了锁,该锁可能覆盖整个文件运行时间。不要用范围过大的文件级锁,却以为它只锁住一个测试。
⚠️ 锁名是资源协调标记,不是收件箱隔离设置。给两个资源不同的锁名,不会让它们变成两个独立收件箱;也不会阻止其他没有声明相同锁名的代码访问该资源。
执行阶段:只让真正争用的用例互斥
共享同一个测试账号或收件资源的用例,应声明同一个锁名;不访问该资源的用例则不应加上这把锁。这样 Playwright 可以让锁定资源的测试依次执行,同时保留其他独立测试的并行能力。不要把“全套测试都改为单 worker”当成默认修复:那会牺牲整个套件的并行性,却未必能解决邮件归属不清的问题。
按条件做决定:
- 若多条用例确实读写同一个账号、收件箱或接口,且并发执行会互相干扰,选相同命名锁。
- 若资源只在单个用例或 worker 内使用,选独立账号或独立收件目标,不要额外锁住无关测试。
- 若用例不共享资源,但可能读到旧邮件或错配验证码,选唯一邮件关联标识与断言;锁解决不了这个问题。
- 若重试会读取前一次遗留的邮件,先修复清理、筛选和关联逻辑;不要把重试再次成功当作隔离已可靠的证据。
这里有个边界:Playwright 文档明确说明,锁可以跨文件、worker 和 project 协调同名测试;不要把它当成独立 CI 作业之间的分布式锁。若不同测试运行会同时访问同一资源,应在运行边界之外另行隔离资源或协调访问。
收到邮件后:用关联证据核对当前运行
在测试邮件中加入可区分的标识,最好能从触发请求一路追踪到实际收到的邮件。例如在自家产品的测试环境里,把运行标识写入测试专用字段或可检索的邮件元数据;如果产品设计允许,也可用专属收件目标区分用例。关键是读取邮件时确实能查验此标识,而不只是把它打印到测试日志里。
test('注册验证码对应本次运行', {
lock: 'inbox:signup-qa',
}, async ({ page }, testInfo) => {
const marker =
`${testInfo.project.name}-${testInfo.testId}-retry-${testInfo.retry}`;
// 仅在自家测试环境中将 marker 与注册请求关联。
// 读取邮件时,除核对验证码外,还要断言邮件包含当前 marker。
// expect(email.marker).toBe(marker);
});
上面的 marker 只是测试数据关联思路,不是 Playwright 会自动注入邮件的功能。你需要让自家产品或测试环境支持传递、保存并读取这个标识。testInfo.testId 可标识测试用例,testInfo.retry 可识别首次运行与重试;跨独立 CI 运行时,还要加入由 CI 提供的运行标识,避免不同运行共享同一关联值。(Playwright TestInfo API 文档)
读取邮件时,至少核对收件目标、运行标识、当前测试状态,以及验证码或链接是否仍对应当前流程。只取“最新一封”不是充分的归属证据:旧邮件可能更晚才被处理,新邮件也可能来自另一个正在运行的测试。
如果你正在补齐测试数据与收件处理流程,可以参照这份QA 邮件测试检查清单,把触发、接收、关联断言和清理分别验收。
复跑阶段:验证重试和清理是否仍然隔离
Playwright 的重试会重新运行失败用例;官方提供 testInfo.retry 来区分重试状态。测试因此可能再次触发发信、重用账号或遇到首次运行残留的邮件。失败后 worker 也可能被替换,所以不要依赖某个 worker 的内存变量来证明邮件已经清空或已被读取。(Playwright 重试文档)
复跑时逐项确认:
- 重试是否使用与首次运行相同的账号和收件目标;若会复用,是否能区分两次运行产生的邮件。
- 旧邮件是在触发新请求前清理,还是由读取逻辑按运行标识过滤;清理动作是否会与另一条测试同时访问同一收件资源。
- 清理接口和读信接口是否都由相同锁保护。只锁“发起注册”的用例,却让清理和读信并发,隔离仍可能被破坏。
- 日志是否能区分首次运行、重试和当前邮件关联标识。不要记录真实用户的验证码、个人信息或可直接使用的重置链接。
若你需要梳理邮件保留与测试数据清理边界,可进一步参考测试数据生命周期说明。用共享收件箱时,测试规则也要符合实际收件和清理能力;不要把“锁已释放”误认为“旧邮件已删除”。
验收阶段:留下锁,还是改造测试资源?
验收标准分成两项,不要合并成一个“测试通过”。
- 资源隔离通过:同名锁覆盖了所有会竞争该共享资源的测试;未争用该资源的用例仍可并行;清理操作也包含在适当的协调范围内。
- 邮件归属通过:当前测试能证明读到的邮件对应本次运行,并核对验证码或链接与当前产品状态相符;首次运行和重试不会把彼此的邮件混用。
若失败只由共享资源被并发读写造成,保留锁,并把适用资源和覆盖范围写入测试注释。若旧邮件仍可能被读到,或无法证明邮件属于当前运行,应增加关联标识、改用独立账号或隔离收件目标。Playwright 的命名锁只能协调同一次测试运行中声明同名锁的用例;它不是端到端收件验收,也不保证验证码正确。
常见决策:锁能解决什么,不能解决什么?
命名锁能防止并行测试读错验证码吗?不能保证。它可以阻止共享同名锁的测试同时访问受保护资源,却不会判断某封邮件属于哪个测试运行。要验证归属,仍需唯一标识、精确筛选和验证码或链接断言。
怎样判断锁的范围是否过大?检查锁覆盖的每个用例是否真的访问同一共享资源。跨文件或 project 使用同名锁是受支持的;但把无关的收件箱、账号也放进同一锁,会造成不必要的等待。对于不同运行之间的共享资源,还要另行处理运行级协调。
什么情况下独立收件目标更合适?当邮件残留、延迟到达或跨运行并发使你无法可靠判断邮件归属时,优先使用独立收件目标或账号。若只是同一运行中少数用例争用一个无法并发访问的共享资源,且邮件关联断言充分,命名锁更直接。
如果你已确认测试必须接收真实邮件,再选择与自家 QA 环境相符的收件方式。临时注册、试用或只收一次验证码时,MaskSend 可以作为不暴露主邮箱的方案之一;使用前先核对页面实际提供的收件能力与保留策略。需要长期找回、持续收信或回复邮件的账号,则应使用适合长期管理的测试邮箱,而不是依赖临时收件。
常见问题
给测试加了命名锁,验证码还会串吗?
仍有可能。命名锁只让声明同一锁名的测试避免同时执行,不会识别收件箱里的某封邮件是不是当前用例触发的。旧邮件、延迟到达的邮件、其他测试运行写入的邮件,仍可能被读取。让自家产品测试邮件携带可区分的运行标识,并断言验证码或链接与当前状态相符;若无法做到,再隔离收件目标。
跨测试文件或 project 使用同一个锁名可以吗?
可以,Playwright 官方文档说明,共享锁名的测试可跨文件、worker 和 project 互斥。各处的锁名必须完全一致,而且都由同一次 Playwright 测试运行协调;另一个独立启动的 CI 作业不会因为用了相同字符串就自动排队。先检查项目安装版本支持的写法,再把锁声明在确实共享资源的测试或 describe 组上。
共享收件箱时,怎样确认邮件属于当前用例?
在自家产品的授权测试环境中,为每次运行生成唯一标识,并让注册请求与测试邮件都能携带或查询这个标识。读取邮件时,除了核对验证码或链接,还要校验该标识、收件地址和当前测试状态;不要只取收件箱最新一封。日志记录运行标识和重试序号即可,避免输出真实用户信息或完整验证码。
什么时候该换独立账号或隔离收件目标?
如果锁让同一资源不再被并发读写,但用例仍可能读到旧邮件、其他运行的邮件,或无法证明邮件属于当前请求,就应改造测试数据。优先为用例或 worker 分配独立测试账号、唯一收件地址或隔离收件目标。若冲突只来自少数用例同时操作同一个共享账号,且邮件关联断言可靠,保留范围明确的锁通常更合适。
立即测试收信
为短期注册提供一次性收件箱,为长期关系提供独立转发别名。把真实邮箱留给值得信任的通信。