Playwright 的三类断言:会重试的、不重试的、软断言

2026-08-18

写端到端用例的人多半都遇到过这个场面:本地跑得好好的一条断言,进了 CI 就时不时红一次,重跑又绿了。翻日志会发现失败的往往不是点击,而是点击之后那一行 expect。这时候真正该问的不是”要不要加个等待”,而是——你刚才那行 expect 到底会不会重试

Playwright 仓库把断言分成了几节来讲,docs/src/test-assertions-js.md 里依次是 Auto-retrying assertions、Non-retrying assertions、Asymmetric matchers、Negating matchers、Soft assertions。后两类不是独立的断言种类:Asymmetric matchers 那节的原话是这些表达式可以嵌套进别的断言里做宽松匹配,Negating matchers 那节讲的是在 matcher 前面加 .not 取反。真正决定运行时行为的是三条线:重试与否、失败后中不中断、以及你用的是哪个语言绑定

第一类:会重试的断言,判定方式是看断言对象

文档里 Auto-retrying assertions 一节写明:这些断言会一直重试,直到断言通过或者到达断言超时;并且提醒它们是异步的,在 JavaScript 里必须 await

await expect(page.getByTestId('status')).toHaveText('Submitted');

这段来自 docs/src/test-assertions-js.md。文档对它的描述是:Playwright 会反复重新获取那个元素并检查,直到条件满足或超时。

判定方式其实很机械——看你把什么东西传给了 expect。那张 Auto-retrying 表里的条目,接受的都是 locatorpage 或者 responseexpect(locator).toBeVisible()expect(page).toHaveURL()expect(response).toBeOK()。对应的类文档分别是 docs/src/api/class-locatorassertions.mddocs/src/api/class-pageassertions.mddocs/src/api/class-apiresponseassertions.md。换句话说,只要断言的主语是页面上的东西,它就是会重试的那一类

超时来自哪里?docs/src/test-timeouts-js.md 的表格里 Expect timeout 一行给的是 5_000 msdocs/src/test-api/class-testconfig.mdTestConfig.expecttimeout 字段描述也是”defaults to 5000ms”。这是仓库当前文档里的默认值,随版本可能变动。改法有两处,配置里统一改:

export default defineConfig({
  expect: {
    timeout: 10000,
  },
});

或者单条断言上改:await expect(locator).toHaveText('hello', { timeout: 10_000 });。这里的 10000 只是仓库文档里的示例值,不是推荐值。

第二类:不重试的断言,它只是普通的值比较

Non-retrying assertions 那张表里是 toBetoEqualtoContaintoHaveLengthtoThrow 这些,对应 docs/src/api/class-genericassertions.md。文档对这一节的定性很直接:它们能测任何条件,但不会自动重试;而网页大多是异步呈现信息的,用不重试的断言容易写出 flaky 的用例。文档给出的建议是优先用会重试的那类,复杂条件确实需要重试时改用 expect.pollexpect.toPass

expect.poll 是把一个同步的 expect 变成轮询版本:

await expect.poll(async () => {
  const response = await page.request.get('https://api.example.com');
  return response.status();
}, {
  // Custom expect message for reporting, optional.
  message: 'make sure API eventually succeeds',
  // Poll for 10 seconds; defaults to 5 seconds. Pass 0 to disable timeout.
  timeout: 10000,
}).toBe(200);

expect.toPass 则是把一整块代码反复跑到不抛错为止。这两个都能指定 intervals,文档注释里写的默认探测间隔是 [100, 250, 500, 1000]——同样是当前文档里的默认值,随版本可能变动。

这里有一个反直觉的点,是仓库文档自己特意点出来的:toPass 默认 timeout 为 0,并且不遵循自定义的 expect timeout。也就是说你在配置里把 expect.timeout 调大,对 toPass 不起作用;TestConfig.expect 下另有一个独立的 toPass 对象,带自己的 timeoutintervals。如果你曾经把 toPass 当成”跟其它断言一样受全局超时管”的东西,那这就是它偶尔行为不符合预期的原因。

第三类:软断言,关键在失败怎么累积

软断言的定义是:断言失败不终止用例执行,但会把用例标记为失败。写法是 expect.soft(...)

只写到这里是不够用的,重点在后半段——失败去哪儿了。docs/src/test-assertions-js.md 给的是这一段:

// Make a few checks that will not stop the test when failed...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');

// Avoid running further if there were soft assertion failures.
expect(test.info().errors).toHaveLength(0);

TestInfo.errorsdocs/src/test-api/class-testinfo.md 里的类型是 Array<TestInfoError>,描述是”用例执行期间抛出的错误”;同一文件里 TestInfo.error 的描述写明它等于 errors 的第一个元素。所以软断言的失败并没有消失,而是攒在这个数组里,你可以在任意时刻读它,判断”要不要继续往下走”。docs/src/best-practices-js.md 的 Use Soft assertions 一节对这个机制的说法是:软断言不会立刻终止执行,而是在用例结束后汇总并展示一份失败断言的清单。

还有两个组合用法值得记住:expect.configure({ soft: true }) 可以造一个默认走软断言的 expect 实例;expect.soft.poll(...) 可以让轮询里的断言也走软失败。

一句限定要跟着写:JS 侧文档明确说明软断言只在 Playwright test runner 下工作。你如果是拿 Playwright 当库、跑在别的测试框架里,这条不成立。

跨语言:同一件事,四种绑定不一样

这一节容易踩坑,因为社区博客大多只写 JavaScript。仓库里 docs/src/test-assertions-js.mddocs/src/test-assertions-csharp-java-python.md 是两份分开的文档,内容并不对齐。

不重试的那一类,在非 JS 绑定里没有对应物。 docs/src/api/class-genericassertions.md 头部标的是 * langs: jsPlaywrightAssertions.expectGeneric 也标 langs: js。而 test-assertions-csharp-java-python.md 只有一张 List of assertions 表,里面全是 LocatorAssertionsPageAssertionsAPIResponseAssertions 的条目——也就是全是会重试的那一类。至于 Python、Java、C# 里普通值比较该怎么写,这一点我们在仓库文档里没有找到统一的说明,不比。

取反的写法分两派。 LocatorAssertions.not 这个属性标的是 langs: java, js, csharp

assertThat(locator).not().containsText("error");

Python 走的是另一条路——class-locatorassertions.md 里给 Python 单独定义了 NotToBeAttachedNotToBeChecked 这一整套方法,落到 Python 代码里就是 not_to_be_visible() 这种形式,docs/src/touch-events.md 里的 Python 示例用的正是它。所以从 JS 的 .not.toBeVisible() 平移到 Python 时不能照抄链式取反。

软断言目前只有 JS 与 Python 两份说明。 Python 那份是同步写法,并且带了一条硬性前提:

expect.soft(page.get_by_test_id("status")).to_have_text("Success")

文档写明它只在 pytest-playwright(或 pytest-playwright-asyncio)插件 0.8.0 或更新的版本上工作。Java 与 C# 的软断言,我们在这两份断言文档里没有找到对应说明,不比。同样地,expect.pollexpect.toPass 只出现在 JS 那份文档里,另一份里没有对应章节。

全局超时的入口四种绑定各不相同。 JS 在配置文件的 expect.timeout;Python 是 conftest.pyexpect.set_options(timeout=10_000);C# 在测试类初始化里调 SetDefaultExpectTimeout(10_000);Java 则是 PlaywrightAssertions.setDefaultAssertionTimeout(30_000),这个方法在 class-playwrightassertions.md 里标 langs: java。上面这些数值都是各自文档里的示例值。

以上代码片段均取自仓库文档,未做改写;组合到你自己的工程里时,以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。断言这一层我们在仓库文档里没有看到 Windows 与 Linux/macOS 的行为差异说明,因此不分平台展开;差异更可能出现在浏览器安装与 CI 环境那一层,不在本文范围内。

从处境倒推:该用哪一类

  • 你在断言页面上的某个东西(文本、可见性、URL、响应状态)→ 用会重试的那一类,别自己写等待。它本来就会重新获取元素反复检查。
  • 你在断言一个已经拿到手的普通值(数组长度、对象结构、抛没抛错)→ 只能用不重试的那类,但要意识到它是一次性判定;如果这个值本身是异步才稳定下来的,改用 expect.poll(断言返回值)或 expect.toPass(重试整块代码),并且记得给 toPass 单独配超时。
  • 一条用例里要检查多个互相独立的点,你不想第一个失败就看不到后面→ 用 expect.soft,并在关键分叉前用 test.info().errors 判一次要不要继续。前提是你跑在 Playwright test runner 上;Python 侧还要满足那条插件版本要求。
  • 你写的不是 JavaScript → 先去 docs/src/test-assertions-csharp-java-python.md 确认你要的那个机制在你的绑定里有没有。三类里只有”会重试的”这一类是四种绑定都写明的。

这个差异什么时候会咬到你:当你把一套 JS 的用例约定搬到 Python 团队,或者反过来。取反写法、软断言的可用性、轮询工具的有无,都不是”语法糖不同”,而是文档里就没写对应能力。照着 JS 的经验去 Python 里找 expect.poll,会浪费掉一个下午。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。