断言
简介
Playwright 以 expect
函数的形式提供了测试断言功能。要进行断言,调用 expect(value)
并选择一个能反映期望的匹配器。有许多通用匹配器如 toEqual
、toContain
、toBeTruthy
可用于断言任何条件。
expect(success).toBeTruthy();
Playwright 还包含了专门针对网页的异步匹配器,这些匹配器会等待直到满足预期条件。请看以下示例:
await expect(page.getByTestId('status')).toHaveText('Submitted');
Playwright 会不断重新测试带有 status
测试ID的元素,直到获取的元素包含 "Submitted"
文本为止。它会反复获取元素并检查,直到满足条件或达到超时时间。你可以通过传递这个超时参数,或者在测试配置中通过 testConfig.expect 值一次性配置。
默认情况下,断言超时时间设置为5秒。了解更多关于各种超时设置的信息。
自动重试断言
以下断言会不断重试,直到断言通过或达到断言超时时间。请注意,重试断言是异步操作,因此必须使用 await
。
非重试断言
这些断言可用于测试任何条件,但不会自动重试。大多数情况下,网页会异步显示信息,使用非重试断言可能导致测试不稳定。
尽可能优先使用自动重试断言。对于需要重试的更复杂断言,可以使用 expect.poll
或 expect.toPass
。
否定匹配器
通常,我们可以通过在匹配器前添加 .not
来表示相反的期望:
expect(value).not.toEqual(0);
await expect(locator).not.toContainText('some text');
软断言
默认情况下,失败的断言会终止测试执行。Playwright 还支持软断言:失败的软断言不会终止测试执行,但会将测试标记为失败。
// 进行一些检查,即使失败也不会停止测试...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');
// ...然后继续测试以检查更多内容。
await page.getByRole('link', { name: 'next page' }).click();
await expect.soft(page.getByRole('heading', { name: 'Make another order' })).toBeVisible();
在测试执行的任何时刻,你都可以检查是否存在软断言失败:
// 进行一些检查,即使失败也不会停止测试...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');
// 如果存在软断言失败,则避免继续执行。
expect(test.info().errors).toHaveLength(0);
请注意,软断言仅适用于 Playwright 测试运行器。
自定义 expect 消息
您可以将自定义的 expect 消息作为第二个参数传递给 expect
函数,例如:
await expect(page.getByText('Name'), '应该已登录').toBeVisible();
该消息会在报告器中显示,无论是通过还是失败的 expect 断言,都能为断言提供更多上下文信息。
当 expect 通过时,您可能会看到如下成功步骤:
✅ 应该已登录 @example.spec.ts:18
当 expect 失败时,错误信息会显示如下:
错误:应该已登录
调用日志:
- expect.toBeVisible 超时 5000ms
- 等待 "getByText('Name')"
2 |
3 | test('示例测试', async({ page }) => {
> 4 | await expect(page.getByText('Name'), '应该已登录').toBeVisible();
| ^
5 | });
6 |
软断言也支持自定义消息:
expect.soft(value, '我的软断言').toBe(56);
expect.configure
您可以创建自己预配置的 expect
实例,以设置自己的默认值,如 timeout
和 soft
。
const slowExpect = expect.configure({ timeout: 10000 });
await slowExpect(locator).toHaveText('提交');
// 始终执行软断言
const softExpect = expect.configure({ soft: true });
await softExpect(locator).toHaveText('提交');
expect.poll
你可以使用 expect.poll
将任何同步的 expect
转换为异步轮询形式。
以下方法会持续轮询给定函数,直到它返回 HTTP 状态码 200:
await expect.poll(async () => {
const response = await page.request.get('https://api.example.com');
return response.status();
}, {
// 可选的自定义断言消息,用于报告
message: '确保 API 最终成功',
// 轮询 10 秒;默认为 5 秒。传入 0 可禁用超时
timeout: 10000,
}).toBe(200);
你也可以指定自定义的轮询间隔:
await expect.poll(async () => {
const response = await page.request.get('https://api.example.com');
return response.status();
}, {
// 探测 → 等待 1 秒 → 探测 → 等待 2 秒 → 探测 → 等待 10 秒 → 探测 → 等待 10 秒 → 探测
// ... 默认为 [100, 250, 500, 1000]
intervals: [1_000, 2_000, 10_000],
timeout: 60_000
}).toBe(200);
你可以将 expect.configure({ soft: true })
与 expect.poll 结合使用,在轮询逻辑中执行软断言:
const softExpect = expect.configure({ soft: true });
await softExpect.poll(async () => {
const response = await page.request.get('https://api.example.com');
return response.status();
}, {}).toBe(200);
这样即使轮询内的断言失败,测试也会继续执行。
expect.toPass
您可以重试代码块直到它们成功通过。
await expect(async () => {
const response = await page.request.get('https://api.example.com');
expect(response.status()).toBe(200);
}).toPass();
您还可以指定自定义超时和重试间隔:
await expect(async () => {
const response = await page.request.get('https://api.example.com');
expect(response.status()).toBe(200);
}).toPass({
// 探测,等待1秒,探测,等待2秒,探测,等待10秒,探测,等待10秒,探测...
// 默认为 [100, 250, 500, 1000]
intervals: [1_000, 2_000, 10_000],
timeout: 60_000
});
请注意,默认情况下 toPass
的超时时间为 0,并且不会遵循自定义的 expect timeout。
使用 expect.extend 添加自定义匹配器
您可以通过提供自定义匹配器来扩展 Playwright 的断言功能。这些匹配器将在 expect
对象上可用。
在这个示例中,我们添加了一个自定义的 toHaveAmount
函数。自定义匹配器应返回一个 pass
标志表示断言是否通过,以及一个在断言失败时使用的 message
回调函数。
import { expect as baseExpect } from '@playwright/test';
import type { Locator } from '@playwright/test';
export { test } from '@playwright/test';
export const expect = baseExpect.extend({
async toHaveAmount(locator: Locator, expected: number, options?: { timeout?: number }) {
const assertionName = 'toHaveAmount';
let pass: boolean;
let matcherResult: any;
try {
const expectation = this.isNot ? baseExpect(locator).not : baseExpect(locator);
await expectation.toHaveAttribute('data-amount', String(expected), options);
pass = true;
} catch (e: any) {
matcherResult = e.matcherResult;
pass = false;
}
if (this.isNot) {
pass =!pass;
}
const message = pass
? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: not ${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
: () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
`Locator: ${locator}\n` +
`Expected: ${this.utils.printExpected(expected)}\n` +
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');
return {
message,
pass,
name: assertionName,
expected,
actual: matcherResult?.actual,
};
},
});
现在我们可以在测试中使用 toHaveAmount
匹配器。
import { test, expect } from './fixtures';
test('amount', async () => {
await expect(page.locator('.cart')).toHaveAmount(4);
});
与 expect 库的兼容性
:::注意
请不要将 Playwright 的 expect
与 expect
库混淆。后者并未完全集成到 Playwright 测试运行器中,因此请确保使用 Playwright 自带的 expect
。
:::
合并来自多个模块的自定义匹配器
您可以将来自多个文件或模块的自定义匹配器进行合并。
import { mergeTests, mergeExpects } from '@playwright/test';
import { test as dbTest, expect as dbExpect } from 'database-test-utils';
import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils';
export const expect = mergeExpects(dbExpect, a11yExpect);
export const test = mergeTests(dbTest, a11yTest);
import { test, expect } from './fixtures';
test('passes', async ({ database }) => {
await expect(database).toHaveDatabaseUser('admin');
});