Playwright Test 和 Playwright Library:两种用法什么时候选哪个
第一次装 Playwright 的人经常在这里卡一下:搜到的教程一半写 npm i -D playwright,另一半写 npm init playwright@latest;一半的代码 import { chromium } from 'playwright',另一半 import { test, expect } from '@playwright/test'。抄错一边,代码跑不通,报错还看不出所以然。
这不是版本新旧的差别。Playwright 仓库里 docs/src/library-js.md 这一页专门讲的就是这件事,开头一句写得很直白:Playwright Library 提供统一的浏览器启动与交互 API,而 Playwright Test 在这之上还提供一整套托管的端到端 Test Runner 与配套体验;对端到端测试来说,多数情况下你要的是 @playwright/test,而不是直接用 playwright。
多数情况不等于所有情况。下面按仓库里白纸黑字写明的东西逐条过,看这条分界线到底划在哪。
先把两段代码摆在一起
docs/src/library-js.md 给了同一件事的两个版本。Library 那版原文是这样(启动 Chromium、按 iPhone 11 的设备参数建上下文、拦掉 jpg 请求、访问页面、检查标题):
import { chromium, devices } from 'playwright';
import assert from 'node:assert';
(async () => {
// Setup
const browser = await chromium.launch();
const context = await browser.newContext(devices['iPhone 11']);
const page = await context.newPage();
// The actual interesting bit
await context.route('**.jpg', route => route.abort());
await page.goto('https://example.com/');
assert(await page.title() === 'Example Domain'); // 👎 not a Web First assertion
// Teardown
await context.close();
await browser.close();
})();
文档写明它用 node my-script.js 运行。Test 那版原文是:
import { expect, test, devices } from '@playwright/test';
test.use(devices['iPhone 11']);
test('should be titled', async ({ page, context }) => {
await context.route('**.jpg', route => route.abort());
await page.goto('https://example.com/');
await expect(page).toHaveTitle('Example');
});
用 npx playwright test 运行。
把两段并排看,中间那三行「真正干活」的代码几乎一样:context.route()、page.goto()。不一样的是它上下的东西——上面的 Setup 和下面的 Teardown 在 Test 版里消失了,断言从 assert 换成了 await expect(page).toHaveTitle()。
仓库列出的差异,逐条读
library-js.md 里那张 Key Differences 表,我按会咬到人的顺序重排一下:
初始化。 Library 侧文档写明你要显式做四件事:选一个浏览器(例如 chromium)、用 BrowserType.launch 启动、用 Browser.newContext 建上下文并把上下文选项显式传进去(例如 devices['iPhone 11'])、再用 BrowserContext.newPage 建页面。Test 侧文档的说法是:每个测试开箱即得到隔离的 page 与 context,以及其它内置 fixture;不需要显式创建,只要测试在参数里引用了它们,Test Runner 就会为这个测试创建——文档明确称之为惰性初始化(lazy-initialization)。
docs/src/test-fixtures-js.md 把这些内置 fixture 列了出来:page 是本次测试隔离的 [Page],context 是本次测试隔离的 [BrowserContext] 且 page 属于它,browser 这一条的原文说明是浏览器在测试之间共享以优化资源,另外还有 browserName 和 request。注意 context 隔离而 browser 共享——这两句是同一张表里挨着的两行,读的时候容易只记住「隔离」两个字。
清理。 Library 侧要你显式关 BrowserContext.close 和 Browser.close;Test 侧文档写明内置 fixture 不需要你显式关闭,Test Runner 会处理。这就是上面那段代码里 Teardown 消失的原因。
断言。 Library 侧的原话是没有内置的 Web-First 断言,所以示例里用了 Node 自带的 assert,仓库还在那行后面加了个「👎 not a Web First assertion」的注释。Test 侧提供 docs/src/test-assertions-js.md 里那套 expect,文档写明这类断言会自动等待并重试直到条件满足;test-assertions-js.md 举的例子是 await expect(page.getByTestId('status')).toHaveText('Submitted'),说明它会反复重新取元素、反复检查,直到条件满足或超时。
超时。 这一条最容易读反。表里 Library 一栏写的是多数操作默认 30s 超时;Test 一栏写的是多数操作不超时,但每个测试有一个会让它失败的超时(默认 30s)。是「操作各自超时」和「整个测试一把超时」两种模型,不是同一个数字换了个位置。这两个 30s 是仓库当前文档里的默认值,随版本可能变动。
运行方式。 Library 侧文档说你是把代码当 node 脚本跑,可能需要先编译;Test 侧说你用 npx playwright test,编译、跑什么、怎么跑都由 Test Runner 结合配置文件处理。
Test Runner 那一侧多出来的东西
library-js.md 在表格之后另起一段,列出 Playwright Test 作为完整 Test Runner 还包含的能力,并各自给了文档链接。这几项就是你选 Library 时要自己接管的部分:
配置矩阵与 projects。 文档自述的理由是:在 Library 版里如果想换个设备或浏览器跑,得改脚本、把信息一路传下去;用 Test 则在一处写好配置矩阵,同一个测试会在每种配置下各跑一遍。docs/src/test-configuration-js.md 的示例配置里 projects 数组的第一项就是 { name: 'chromium', use: { ...devices['Desktop Chrome'] } }——这是仓库里的示例值。
并行。 docs/src/test-parallel-js.md 写明测试跑在多个 worker 进程里,默认是测试文件之间并行,同一文件内的测试在同一个 worker 里按顺序跑;想让单文件内并行要写 test.describe.configure({ mode: 'parallel' }),想让整个 project 全并行用 fullyParallel,想关掉并行就把 worker 限成一个。worker 数量可以命令行传,也可以配置文件里写——文档给的命令行示例是 npx playwright test --workers 4,其中的 4 只是仓库文档里的示例值。文档还写明 worker 之间不能通信,并行的测试跑在不同进程里、不能共享状态或全局变量,每个测试自己执行一遍相关的钩子,包括 beforeAll 和 afterAll。
报告。 docs/src/test-reporters-js.md 写明内置了若干 reporter,可以用 --reporter=line 这样的命令行参数试,也可以在配置里写;支持同时挂多个,文档给的例子是 reporter: [['list'], ['json', { outputFile: 'test-results.json' }]]。docs/src/intro-js.md 写明 HTML 报告只在有失败时自动打开,其余时候用 npx playwright show-report 手动开。
重试。 docs/src/test-retries-js.md 讲了失败后的进程语义:任何测试失败,Test Runner 会连同浏览器一起丢弃整个 worker 进程、另起一个,从下一个测试继续;开了 retries 的话,新进程会先重试那个失败的测试再往下走。
trace。 这一项是我觉得最值得单独说的。docs/src/api/class-tracing.md 说明 Library 侧照样能录 trace,示例里是 context.tracing.start({ screenshots: true, snapshots: true }) 开始、context.tracing.stop({ path: 'trace.zip' }) 结束。但同一页有一条 note 写明:context.tracing 这套 API 捕获的是浏览器操作与网络活动,不记录测试断言(比如 expect 调用),因此推荐通过 Playwright Test 的配置开启 tracing,那样得到的 trace 更完整。配置侧的示例是 trace: 'on-first-retry',注释写的是「Collect trace when retrying the failed test」——这是仓库里的示例值。
换句话说,trace 不是 Test Runner 独占的能力,但「trace 里能看到断言」和「失败重试时才录」这两件事挂在 Test Runner 一侧。
安装侧也不一样,别混着抄
library-js.md 的表里连安装都分开写了:Library 是 npm install playwright,Test 是 npm init playwright@latest——文档特意提醒注意 install 和 init 的差别。浏览器的获取方式也不同:Test 侧用 npx playwright install 或 npx playwright install chromium 装单个;Library 侧文档给的是装 @playwright/browser-chromium、@playwright/browser-firefox、@playwright/browser-webkit 这几个包,它们会在 npm 安装时顺带下载对应浏览器。文档也写了 Library 侧同样可以直接 npx playwright install chromium firefox webkit。装好之后可以在 Node 脚本里 import Playwright,启动 chromium、firefox、webkit 这三个浏览器中的任意一个。
Windows 这边要注意的是环境变量写法。 library-js.md 讲代理与下载源时,bash、batch、PowerShell 三种写法是分开给的。走代理下载浏览器,bash 侧原文是 HTTPS_PROXY=https://192.0.2.1 npx playwright install;Windows 的 batch 侧原文是分两行:
set HTTPS_PROXY=https://192.0.2.1
npx playwright install
PowerShell 侧原文是 $Env:HTTPS_PROXY=https://192.0.2.1 再跟 npx playwright install。改从内部制品库下载则把变量换成 PLAYWRIGHT_DOWNLOAD_HOST,文档写明默认是从微软的 CDN 下载。完全跳过浏览器下载用 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1,文档标明这一条是配合上面那几个 helper 包用的。上面的 192.0.2.1 是仓库文档里的示例地址。
顺带说一句,两种用法启动的都是真实浏览器进程,会真的加载页面、执行页面上的脚本。Library 模式下浏览器与上下文的生命周期由你自己的代码负责关闭,这是文档明确列在你这一侧的责任。
那到底怎么选
不用背表,按你的处境往下问三句:
第一句:这段代码的产出物是「通过/失败」吗? 如果最终要的是一份跑完之后能说「哪些用例过了、哪些挂了、挂在哪一步」的结果,还要在 CI 上出报告、失败自动重试、失败时留 trace——那 fixture、并行、reporter、retries 这四样你迟早都要,而它们在仓库文档里都归在 Test Runner 一侧。自己搭一遍不是不行,但你搭的是一个已经存在的东西。
第二句:浏览器是不是某个更大程序里的一个部件? 比如一段抓取脚本、一个生成截图或 PDF 的服务、一个被别的调度器拉起来的任务。这类场景里根本没有「用例」这个概念,也就没有 fixture 要注入、没有报告要出。这时 Library 那套显式的 launch / newContext / newPage / close 反而是你想要的——生命周期本来就该由你的程序控制。
第三句:你已经有别的 runner 了吗? 这一点要分语言看,而且我只写查得到的:Python 侧 docs/src/test-runners-python.md 的标题就是 Pytest Plugin Reference,写明 Playwright 提供 pytest 插件、用 pytest CLI 跑测试,例如 pytest --browser webkit --headed;Python 侧另有独立的 docs/src/library-python.md 讲直接装 playwright 库的用法。至于 JS 侧「能不能拿第三方 runner 驱动 playwright 库」,我们在 library-js.md 里没有找到对应说明,这一点不比。
同样地,「两个包能不能在同一个项目里共存、共存时浏览器二进制怎么算」,我们在这两页里也没有找到专门说明,不替仓库补话。
这个差异什么时候会咬到你
最常见的一种:从网上抄了 Library 版的代码放进 tests/ 目录,再用 npx playwright test 跑。这时你的文件里既有自己 chromium.launch() 出来的浏览器,又有 Test Runner 准备的 fixture,两套生命周期叠在一起,超时模型也是两套——Library 那边多数操作各自 30s,Test 这边整个测试一把 30s。
另一种反过来:在纯 Library 脚本里写 await expect(...).toHaveTitle(...),然后发现 expect 从 playwright 里 import 不出来。按 library-js.md 的说法,Web-First 断言本来就列在 Test 一侧,Library 侧写的是「无内置 Web-First 断言」,示例里用的是 node:assert。
判断自己站在哪一边其实很简单:看你的 import 是 playwright 还是 @playwright/test,看你的启动命令是 node 还是 npx playwright test。这两处必须是同一侧的。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。
本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。