为何你的 Playwright 测试越跑越慢?从同步依赖到并行自治的进化之路

简介: 本文剖析Playwright测试变慢的根源——测试间隐式同步依赖,并提供三大优化方案:①用worker级fixture复用登录态;②通过storageState预存认证状态;③开启fullyParallel与分片。辅以避免硬等待、按需录屏、API准备数据等实践,助你将数十分钟测试压缩至几分钟,实现真正高效并行。

开篇:一个让人头疼的现象
如果你的 Playwright 测试套件从最初的几分钟膨胀到现在的半个多小时,每次 CI 跑完都要等到天荒地老,那你一定遇到过这个问题。

我们团队也走过这条路。从几十个用例扩展到几百个之后,测试开始变得不稳定——周一通过的测试周二突然失败,本地跑得好好的用例到 CI 里随机报错。更要命的是,测试越跑越慢,反馈周期越来越长,整个团队的交付节奏都被拖慢了。

有人可能会说:加机器、加 worker 不就行了?但如果问题出在根子上,加再多资源也只是治标不治本。今天我想聊聊一个经常被忽视的罪魁祸首——测试之间的同步依赖,以及如何让测试从“排队等号”进化到“各跑各的”。

问题出在哪:我们为什么会写出同步依赖的测试?
先说一个场景,你看看熟不熟悉。

有一个用户管理系统的测试套件,包含创建用户、登录用户、更新资料、删除用户这几个测试。最初可能是这样写的:

// ❌ 反面示例:存在隐藏依赖
test('创建新用户', async ({ page }) => {
await page.goto('/register');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password123');
await page.click('#submit');
});

test('登录用户', async ({ page }) => {
await page.goto('/login');
// 这里假设 test@example.com 用户已经存在!
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password123');
await page.click('#submit');
});
这种写法的隐患很明显:第二个测试的成功完全依赖于第一个测试的顺利执行。如果第一个测试失败了,第二个必然跟着失败。更糟糕的是,如果 Playwright 默认并行执行,执行顺序无法保证,第二个可能在第一个之前运行,直接报错。

为什么会有人写出这样的测试?
核心原因就一个:想少写重复代码。

“登录只需要做一次,后面的测试直接用登录后的状态就行了”——这个想法本身没毛病,谁都不想每个测试都从头登录一遍。问题在于实现方式。很多人会用一个全局变量保存登录态,或者依赖某个测试先跑完来做初始化。在 Reddit 和 Playwright 的论坛上,经常能看到有人试图通过添加硬等待来让某个测试先跑。

但这种方式带来的代价是什么?测试越多,跑得越慢。

想象一下赛马比赛,如果每匹马都要等前面那匹跑完了才能出发,那每增加一匹马,总时间就增加一匹马的奔跑时间。顺序执行的测试也是这个道理。

解决方案一:用 Fixture 替代“前置测试”
Playwright 的 fixture 机制是解决这个问题的第一把钥匙。

Fixtures 默认是 test 作用域,每个测试都会拿到一个全新的、隔离的实例。但你也可以把它设为 worker 作用域,让同一个 worker 里的所有测试共享一份。

关键在于:只对只读的、昂贵的资源使用 worker 作用域。

举个例子,一个需要登录才能访问的测试套件:

// ✅ 用 fixture 管理登录态
import { test as base } from'@playwright/test';

// 定义一个 worker 作用域的 fixture
const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'auth.json'// 加载预先保存的登录状态
});
const page = await context.newPage();
await use(page);
await context.close();
},
// 手动指定 worker 作用域
{ scope: 'worker' }
});

test('访问个人资料', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/profile');
// 直接就是登录状态,不用再登录一次
});
这样一来,登录这个“昂贵”的操作只做一次,但每个测试都拿到了一个独立的 page 实例,互不干扰。既避免了重复登录浪费时间,又保证了测试之间的隔离。

解决方案二:storageState——登录态复用神器
如果只是想在测试之间共享登录状态,storageState 是更直接的办法。

先跑一个 setup 脚本把登录态保存下来:

// global-setup.ts
import { chromium } from'@playwright/test';

asyncfunction globalSetup() {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

await page.goto('/login');
await page.fill('#email', 'admin@example.com');
await page.fill('#password', 'password123');
await page.click('#submit');

// 保存登录状态到文件
await context.storageState({ path: 'auth.json' });
await browser.close();
}

exportdefault globalSetup;
然后在配置里加载这个状态:

// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
storageState: 'auth.json',
},
});
之后每个测试启动时就已经是登录状态了。我们团队用这个方法后,每个测试平均节省了 3-5 秒的登录时间,几百个用例下来就是几十分钟的差异。

解决方案三:让测试真正“并行”起来
解决了依赖问题之后,就可以放心大胆地开启并行执行了。

Playwright 默认是以测试文件为单位并行的——不同文件跑到不同的 worker 里。配置起来很简单:

// playwright.config.ts
export default defineConfig({
// CI 环境用 4 个 worker,本地用一半核心数
workers: process.env.CI ? 4 : '50%',
// 开启测试级别的并行(同一个文件里的测试也并行跑)
fullyParallel: true,
});
但这里有个前提:测试之间必须真正独立。如果测试之间共享了可变状态,开了 fullyParallel: true 反而会出问题。

什么样的状态是“可变”的?举个例子:

// ❌ 反面模式:模块级的可变状态
let authToken: string;
let page: Page;

test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
page = await context.newPage();
authToken = await getAuthToken();
});

test('读取用户信息', async () => {
await page.goto('/profile'); // 使用了共享的 page
});

test('更新用户设置', async () => {
await page.click('#settings'); // 同一个 page,状态已经被改了
});
page 对象在多个测试之间共享,一个测试导航走了,另一个测试可能就找不到元素了。这种问题在并行执行时会被放大,因为执行顺序不确定,失败也变得随机。

正确的做法是:每个测试用自己独立的 context 和 page。

进阶:分片(Sharding)——当单机不够用时
当测试数量继续增长,单台机器的资源终归是有限的。这时候可以考虑分片(Sharding)——把测试分摊到多台机器上跑。

4 台机器各跑四分之一

npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
在 CI 里可以这样配置:

GitHub Actions

jobs:
test-shard:
strategy:
matrix:
shard-index:[1,2,3,4]
shard-total:[4]
runs-on:ubuntu-latest
steps:
-run:npxplaywrighttest--shard=${ {matrix.shard-index}}/${ {matrix.shard-total}}
开了 fullyParallel: true 之后,分片会更均匀,因为拆分粒度从文件级别降到了测试级别。

还有几个容易忽略的坑

  1. 硬等待是隐形杀手
    很多人习惯了用 waitForTimeout 等固定时间:

// ❌ 不管实际需要多久,都等 5 秒
await page.waitForTimeout(5000);
正确的做法是等待具体的条件:

// ✅ 等元素出现
await page.locator('.data-loaded').waitFor({ state: 'visible' });

// ✅ 等 API 请求完成
const responsePromise = page.waitForResponse('/api/data');
await page.click('#load-data');
const response = await responsePromise;
硬等待不仅慢,而且不稳定——网络快了浪费时间,网络慢了照样失败。

  1. Trace 和视频不要全程开
    Trace 和视频在调试时非常有用,但如果全程开启会严重拖慢测试。建议配置成只在失败时记录:

// playwright.config.ts
export default defineConfig({
use: {
trace: 'on-first-retry', // 重试时才记录 trace
video: 'on-first-retry', // 重试时才录视频
},
});

  1. 用 API 准备数据,别用 UI
    每个测试都通过 UI 去创建数据,又慢又脆弱。更好的做法是:

// ✅ 通过 API 准备测试数据
test('验证订单详情页', async ({ page }) => {
// 用 API 创建订单
const order = await createOrderViaAPI({
userId: 'test-user',
items: [{ id: 'product-1', quantity: 1 }]
});

// UI 只做展示验证
await page.goto(/orders/${order.id});
await expect(page.locator('.order-status')).toHaveText('待支付');
});
总结:从“排队”到“并行”的进化路径
回头看,让 Playwright 测试从越跑越慢到越跑越快,核心就三件事:

第一步,切断依赖。 别让测试 A 的结果成为测试 B 的前置条件。用 fixture、用 storageState,让每个测试都能独立运行。

第二步,资源复用要克制。 只对“只读的、昂贵的”资源做 worker 级别的共享。page、context 这类可变的东西,该隔离就隔离。

第三步,放心并行。 把 workers 开起来,把 fullyParallel 打开,把分片用上。前提是前两步做好了。

我们团队把一个 45 分钟的测试套件优化到了 8 分钟。不是靠堆机器,而是靠把测试从“排队等号”改成了“各跑各的”。当每个测试都独立、自治、不依赖别人的时候,跑得快就是水到渠成的事。

相关文章
|
7天前
|
人工智能 JSON 自然语言处理
让教学更智慧:用阿里云百炼工作流,自动生成中小学教材内容#小有可为#有温度的AI
通过可视化工作流编排,将大模型推理能力转化为标准化的教学内容生成引擎。教师只需输入教材标题和适用学段,即可自动获得结构完整、符合课程标准的章节内容,大幅降低备课门槛,助力教育资源均衡化。
474 123
|
8天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
451 127
|
16天前
|
Linux 程序员 数据格式
【2026最新】Notepad++下载、安装和使用一篇搞定(附中文版安装包)
Notepad++ 是一款免费开源、轻量高效的 Windows 文本编辑器,支持 C/Python/HTML 等 80+ 语言语法高亮、代码折叠、正则替换、编码转换及插件扩展,专为程序员与文本处理用户打造,完美替代系统记事本。(239字)
|
11天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
781 5
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
3天前
|
人工智能 安全 Cloud Native
Higress 新发布:AI Gateway 能力增强,Gateway API 及其推理扩展持续打磨
增强 AI 网关能力,持续打磨 Gateway API 及其推理扩展。
299 122
|
3天前
|
消息中间件 存储 Kafka
Kafka 原生消息入湖能力上线!一键打通实时流与数据湖
阿里云消息队列 Kafka 版正式上线原生消息入湖能力。
249 121
|
8天前
|
缓存 人工智能 运维
阿里云618百炼大模型Qwen3.7-Max功能、免费试用、订阅计费、配置接入详解
Qwen3.7-MAX是阿里云百炼平台推出的通义千问3.7系列旗舰大语言模型,专为智能体时代复杂任务打造,依托阿里云全域算力与自研技术,在逻辑推理、长文本处理、代码工程、长周期自主执行等领域达到行业顶尖水平。2026年618期间,该模型推出多重免费试用权益、按量计费5折、订阅套餐优惠等专属福利,覆盖个人开发者、团队与企业全场景需求,以下从核心功能、免费试用、订阅计费、配置接入四方面展开详细解析。
463 124

热门文章

最新文章