首页
/ Playwright Test 中的 TestInfo 类详解:在测试运行时掌握状态、附件、快照与超时

Playwright Test 中的 TestInfo 类详解:在测试运行时掌握状态、附件、快照与超时

2026-09-06 15:07:16作者:曹令琨Iris

在 Playwright Test 中,每个测试函数、钩子(beforeEach / afterEach / beforeAll / afterAll)以及测试级 fixture 都会收到一个 testInfo 参数,它是"当前正在运行的测试"的完整运行时上下文。本文基于 Playwright 仓库中的官方 API 文档与 TestInfoImpl 源码实现,系统讲解 TestInfo 的每个属性与方法:如何识别当前测试、对比实际/预期状态、条件性跳过或标记失败、管理重试与并行索引、动态调整超时,以及正确落盘附件、临时文件与快照,帮助你在编写测试与自定义 Reporter 时充分利用测试执行期的全部信息。

TestInfo 概述:它是从哪里来的

TestInfo 包含当前运行中测试的信息,可用于测试函数、四个钩子以及 test-scoped fixture,并提供控制测试执行的工具:附加文件、更新测试超时、判断当前运行的是哪个测试、是否处于重试中等。

import { test, expect } from '@playwright/test';

test('basic test', async ({ page }, testInfo) => {
  expect(testInfo.title).toBe('basic test');
  await page.screenshot(testInfo.outputPath('screenshot.png'));
});

从源码结构看,testInfo 的实际类型是 TestInfoImpl,它在每个 worker 进程执行测试时由 workerMain.ts_runTest 创建,并绑定四个回调:onStepBegin / onStepEnd / onAttach / onTestPaused,用于把步骤事件和附件事件上报给测试框架(Reporter 与 Trace Viewer 的数据来源之一)。构造函数把 test 用例的元数据一次性写入:titletitlePathfilelinecolumntagsfnexpectedStatus 等(见 testInfo.ts 构造函数)。

除第二个参数注入外,测试体内还可以随时通过 test.info() 获取同一个实例(由 testType.ts 提供,非测试运行期调用会直接抛错),快照断言相关的示例中即使用了 test.info().snapshotPath(...) 的写法。

识别当前测试:title、位置与归属

属性 类型 含义
testInfo.title string 传给 test(title, testFunction) 的标题
testInfo.titlePath Array<string> 从测试文件名开始的完整标题路径
testInfo.file string 当前测试声明所在文件的绝对路径
testInfo.line int 当前测试声明所在行号
testInfo.column int 当前测试声明所在列号
testInfo.testId string 与 Reporter API 中 test case id 匹配的测试 id(v1.32 起)
testInfo.fn function 传给 test(title, testFunction) 的测试函数本身
testInfo.project FullProject 来自配置文件的处理后 project 配置
testInfo.config FullConfig 来自配置文件的处理后全局配置

在实现中,file / line / column 直接取自测试用例的 locationtestInfo.ts),因此即使测试被 describe 多层嵌套,位置信息也始终指向 test(...) 声明处。titlePath 则包含文件相对路径与各级 describe 标题,这在自定义 Reporter 中做去重与归档时非常有用:titlePath[0] 是测试文件相对路径,其余是各级标题。

testInfo.fn 暴露的是测试函数本体,适合在 fixture 中判断"是否为参数化测试"或做函数级分析(例如根据函数名做分支处理)。

实际状态与预期状态:status 与 expectedStatus

属性 类型 含义
testInfo.status 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted' 实际状态。测试运行期间为 undefined,在 afterEach 钩子与 fixture 中才有值
testInfo.expectedStatus 同上(不含 timedOut / interrupted 预期状态,通常为 'passed'
testInfo.error TestInfoError? 执行中抛出的第一个错误,等于 errors[0]
testInfo.errors Array<TestInfoError> 执行中抛出的全部错误
testInfo.duration int 测试耗时(毫秒),未结束前恒为 0,可在 afterEach 中使用

expectedStatus 通常为 'passed',但有两种例外:

  • 被跳过的测试(如通过 test.skip)为 'skipped'
  • 被标记为"预期失败"(test.fail)的测试为 'failed'

最典型的用法是在 afterEach 中比较实际状态与预期状态:

import { test, expect } from '@playwright/test';

test.afterEach(async ({}, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus)
    console.log(`${testInfo.title} did not run as expected!`);
});

从源码看,expectedStatus 的调整集中在两处:一是 worker 处理文件级/用例级 annotation 时(workerMain.ts 的 processAnnotationskip / fixme 置为 'skipped'fail 置为 'failed');二是测试体内调用 testInfo.skip() 等运行时标注方法时(见下文 _modifier)。失败时,status_failWithError 写入:超时会得到 'timedOut',其余为 'failed',同时把序列化后的错误压入 errors 数组——error 属性只是 errors[0] 的快捷访问器(testInfo.ts)。

运行时控制测试:skip、fixme、fail、slow

TestInfo 提供与静态 API(test.skip 等)语义一致的运行时方法,都支持两种调用形式:无条件调用,或 method(condition, description) 条件调用。

方法 行为
testInfo.skip() / testInfo.skip(condition, description?) 无条件/条件跳过当前测试,测试立即中止
testInfo.fixme() / testInfo.fixme(condition, description?) 无条件/条件标记"待修复",测试立即中止
testInfo.fail() / testInfo.fail(condition, description?) 无条件/条件标记"应当失败":测试照常运行,Playwright Test 断言它确实失败
testInfo.slow() / testInfo.slow(condition, description?) 无条件/条件标记"慢速",超时时间变为默认值的三倍

这四个方法在实现上统一走 _modifier

  • 四个方法都被 transform.wrapFunctionWithLocation 包裹,因此 description 会自动带上调用位置的 location 信息写入 annotations,Reporter 能显示"在哪里被跳过";
  • skip / fixme 会先把 expectedStatus 置为 'skipped',再抛出 TestSkipError 使测试立即中止;
  • failexpectedStatus 改为 'failed'(若已是 skipped 则不覆盖);
  • slow 调用 timeoutManager.slow() 将超时乘以 3 倍。

_modifier 还会显式拦截一个常见误用:在测试内调用 test.skip(callback) 传回调用形式会抛出带有正确示例的错误提示(testInfo.ts)。典型场景是运行时根据环境特征跳过:

test('only on linux', async ({ page }, testInfo) => {
  testInfo.skip(process.platform !== 'linux', 'Linux-only feature');
  // ...
});

重试、并行与重复执行:retry、parallelIndex、workerIndex、repeatEachIndex

属性 含义
testInfo.retry 重试编号。首次运行恒为 0,第一次重试为 1,依此类推
testInfo.parallelIndex 当前 worker 在 0..workers-1 之间的索引;同时运行的 worker 保证互不相同。worker 重启后(如失败后)新进程沿用同一 parallelIndex
testInfo.workerIndex 运行该测试的 worker 进程的唯一索引。worker 重启后新进程会获得新的唯一 workerIndex
testInfo.repeatEachIndex --repeat-each 命令行参数运行时的唯一重复索引,见 test-cli

retry 在排查 flaky 测试时特别有用——重试前清理服务端状态:

import { test, expect } from '@playwright/test';

test.beforeEach(async ({}, testInfo) => {
  // You can access testInfo.retry in any hook or fixture.
  if (testInfo.retry > 0)
    console.log(`Retrying!`);
});

test('my test', async ({ page }, testInfo) => {
  // Here we clear some server-side state when retrying.
  if (testInfo.retry)
    await cleanSomeCachesOnTheServer();
  // ...
});

关于并行索引,parallelIndexworkerIndex 的区别在于:前者标识"并行槽位"(0 到 workers - 1),后者标识"进程实例"。worker 崩溃重启后,新进程继承 parallelIndex 但获得新的 workerIndex。两者也分别以环境变量 process.env.TEST_PARALLEL_INDEXprocess.env.TEST_WORKER_INDEX 暴露,在 workerMain.ts 中于 worker 启动时写入。详见 并行与分片 以及 重试。

retry 还会直接影响输出目录:outputDir 在重试/重复执行时会追加 -retryN / -repeatN 后缀(testInfo.ts),保证多次运行的产物互不覆盖。

超时管理:timeout 与 setTimeout

  • testInfo.timeout:当前测试的超时(毫秒),0 表示不超时;
  • testInfo.setTimeout(timeout):修改当前正在运行的测试的超时,0 表示不超时。

超时通常配置在配置文件中,但有时需要在运行时动态调整。典型例子是在 beforeEach 里为所有走到该钩子的测试统一延长 30 秒:

import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }, testInfo) => {
  // Extend timeout for all tests running this hook by 30 seconds.
  testInfo.setTimeout(testInfo.timeout + 30000);
});

从源码看,timeoutTimeoutManager 的默认槽读取值(testInfo.ts),而 setTimeout 直接调用 _timeoutManager.setTimeout(timeout)。另外,testInfo.slow() 调用的 timeoutManager.slow() 会将默认槽超时乘以 3 倍。更多超时机制参见 各种超时。

标注与标签:annotations 与 tags

testInfo.annotations 是一个数组,类型为:

type: Array<{
  type: string;              // 标注类型,例如 'skip' 或 'fail'
  description?: string;     // 可选描述
  location?: Location;      // 可选:标注在源码中的位置
}>

它汇总了三层来源的标注:测试自身的标注、测试所属各级 describe 组的标注、以及测试文件级的标注。这与 workerMain 中的 annotation 处理逻辑 对应:先处理测试自身 annotations,再叠加父级 suite 在运行中动态追加的标注。更多标注用法见 test annotations。

testInfo.tags(v1.43 起)返回作用于当前测试的标签数组,例如 ['@smoke'],可用于在 fixture 中按标签启用不同行为。注意:测试运行期间对该列表的修改对 Reporter 不可见(因为事件在上报时已序列化)。标签用法详见 tags 章节。

附件:attach 方法与 attachments 属性

testInfo.attachments 记录附加到当前测试的全部文件或 Buffer,结构为:

type: Array<{
  name: string;           // 附件名
  contentType: string;   // 供报告正确展示的内容类型,如 'application/json'、'image/png'
  path?: string;         // 可选:附件在文件系统中的路径
  body?: Buffer;         // 可选:替代文件使用的附件本体
}>

一些 Reporter 会展示测试附件。添加附件应使用 testInfo.attach(),而不是直接 pushattachments 数组。

attach 的两种数据源:body 与 path

import { test, expect } from '@playwright/test';

test('basic test', async ({ page }, testInfo) => {
  await page.goto('https://playwright.dev');
  const screenshot = await page.screenshot();
  await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});

也可以附加由 API 返回的文件:

import { test, expect } from '@playwright/test';
import { download } from './my-custom-helpers';

test('basic test', async ({}, testInfo) => {
  const tmpPath = await download('a');
  await testInfo.attach('downloaded', { path: tmpPath });
});

参数说明:

参数 说明
name 附件名,会经过 sanitize 并作为落盘文件名前缀
body 附件本体(stringBuffer),与 path 互斥
contentType 报告展示用的内容类型;省略时根据 path 推断,字符串附件默认 text/plain,Buffer 附件默认 application/octet-stream
path 文件系统上的附件文件路径,与 body 互斥

pathbody 必须二选一。重要提示:attach 会自动把附件文件复制到 Reporter 可访问的位置,因此 await 完成后原文件可以安全删除。

从源码看,TestInfoImpl.attach 做了三件事:创建一条 category 为 'test.attach' 的步骤(会出现在 Trace/报告中)、通过 normalizeAndSaveAttachment 把附件归一化并拷贝到 outputPath() 下、再通过 _attach 将附件事件(含 base64 化的 body)经 onAttach 回调上报给框架。这也解释了为什么文档说"可以安全删除原文件"——框架内部已经复制了一份。

每个测试独占的目录:outputDir 与 outputPath

  • testInfo.outputDir:本次测试运行独占的输出目录绝对路径,各次运行互不冲突;
  • testInfo.snapshotDir:本测试的快照输出目录绝对路径,每个测试套件独占一个目录,互不冲突。注意该属性不考虑 snapshotPathTemplate 配置(模板会在此之上进一步插值)。

testInfo.outputPath(...pathSegments) 返回 outputDir 内部的一个路径,测试可安全地在其中写临时文件,并保证并行测试之间互不干扰:

import { test, expect } from '@playwright/test';
import fs from 'fs';

test('example test', async ({}, testInfo) => {
  const file = testInfo.outputPath('dir', 'temporary-file.txt');
  await fs.promises.writeFile(file, 'Put some data to the dir/temporary-file.txt', 'utf8');
});

outputPath 支持多段路径,如 testInfo.outputPath('relative', 'path', 'to', 'output');但结果路径必须仍位于该测试的 outputDir(即 test-results/<test-title>)之内,否则会抛错。这一点在源码中由 _getOutputPath 中的 getContainedPath 校验实现,越界时抛出 The outputPath is not allowed outside of the parent directory

outputDir 的构造规则(testInfo.ts)值得了解:它由"测试文件相对路径(把 / 换成 -)+ 清洗后的完整测试标题"拼成,并依次追加 project id(多 project 时)、-retryN-repeatN 后缀,最终挂在 project.outputDir(默认 test-results)之下。outputPath() 首次调用时还会自动 mkdirSync 创建目录。

快照路径:snapshotPath 方法与 snapshotSuffix

testInfo.snapshotPath(...name) 返回给定名称的快照文件路径。v1.53 起可以传入 kind 指定快照种类,从而匹配对应断言使用的路径模板:

  • kind: 'screenshot' 对应 expect(page).toHaveScreenshot(name)
  • kind: 'aria' 对应 expect(locator).toMatchAriaSnapshot(...)
  • kind: 'snapshot' 对应 expect(value).toMatchSnapshot(name),也是默认值。
await expect(page).toHaveScreenshot('header.png');
// Screenshot assertion above expects screenshot at this path:
const screenshotPath = test.info().snapshotPath('header.png', { kind: 'screenshot' });

await expect(page.getByRole('main')).toMatchAriaSnapshot({ name: 'main.aria.yml' });
// Aria snapshot assertion above expects snapshot at this path:
const ariaSnapshotPath = test.info().snapshotPath('main.aria.yml', { kind: 'aria' });

expect('some text').toMatchSnapshot('snapshot.txt');
// Snapshot assertion above expects snapshot at this path:
const snapshotPath = test.info().snapshotPath('snapshot.txt');

expect('some text').toMatchSnapshot(['dir', 'subdir', 'snapshot.txt']);
// Snapshot assertion above expects snapshot at this path:
const nestedPath = test.info().snapshotPath('dir', 'subdir', 'snapshot.txt');

参数细节:

  • ...name:快照名或定义快照文件路径的路径段。同一测试文件中同名的快照预期相同;
  • 传入 kind 时,不支持多个 name 段;
  • kind 决定使用哪个快照路径模板,详见 TestConfig.snapshotPathTemplate 配置,默认 'snapshot'

实现上,snapshotPath 会校验 kind 取值(非法值抛 unknown kind 错误),再交给 _resolveSnapshotPaths 完成解析:对 screenshot / aria / snapshot 分别选择 expect.toHaveScreenshot.pathTemplateexpect.toMatchAriaSnapshot.pathTemplate 或全局 snapshotPathTemplate(aria 快照在未配置时回退到不含 projectName/snapshotSuffix 插值段的默认模板)。随后 _applyPathTemplate{testDir}{snapshotDir}{testFileName}{arg}{projectName}{snapshotSuffix} 等占位符替换为实际值——这正是 kind 能精确复现断言落盘位置的原因。

testInfo.snapshotSuffix 用于在多种测试配置之间区分快照(例如让 snapshotSuffix = process.platform 按平台使用不同快照)。官方已不建议继续依赖该属性,推荐改用 TestConfig.snapshotPathTemplate 配置快照路径。快照机制详见 test snapshots。

小结:TestInfo 能力速查

能力 成员 典型场景
识别测试 titletitlePathfilelinecolumntestIdfn 自定义 Reporter、日志定位
结果判定 statusexpectedStatuserrorerrorsduration afterEach 中告警、统计耗时
运行时控制 skipfixmefailslow(均可带 condition/description) 按环境/数据条件动态调整执行
重试与并行 retryparallelIndexworkerIndexrepeatEachIndex(另有 TEST_RETRYTEST_PARALLEL_INDEXTEST_WORKER_INDEX 环境变量) 重试清理、按 worker 分片资源
超时 timeoutsetTimeout 钩子内统一延长超时
标注 annotationstags 汇总文件级/组级/测试级标注
产物落盘 attachattachmentsoutputDiroutputPath 截图/日志附件、并行安全的临时文件
快照 snapshotDirsnapshotPath(含 kind)、snapshotSuffix 复现断言的快照路径、多配置区分快照

以上所有 API 自 v1.10 起可用(tags 自 v1.43、snapshotPathkind 选项自 v1.53、testId 自 v1.32)。完整的类型声明见 packages/playwright/types/test.d.ts,运行实现见 packages/playwright/src/worker/testInfo.ts

登录后查看全文
热门项目推荐
相关项目推荐