01 为什么你的Puppeteer测试像蜗牛在爬?
大多数前端工程师写E2E测试时,都踩进同一个坑:串行执行。一个测试跑完,下一个才开始。100个测试用例,每个耗时3秒,就是300秒。加上页面加载、资源请求、动画等待,500秒起步。
这不是Puppeteer的问题,是你的策略问题。
三大性能杀手:
1. 串行执行:测试用例排队等待,CPU利用率不到10%
2. 懒加载不处理:图片、组件按需加载,测试等待超时
3. 依赖未优化:每个测试都重新登录、初始化,重复造轮子
看一个典型的慢测试代码:
javascript
// 慢速测试:串行执行
const { chromium } = require('puppeteer');
async function runTests() {
const browser = await chromium.launch();
// 测试1:登录
const page1 = await browser.newPage();
await page1.goto('https://example.com/login');
await page1.type('#username', 'testuser');
await page1.type('#password', 'password123');
await page1.click('#login-btn');
await page1.waitForNavigation();
console.log('登录测试通过');
await page1.close();
// 测试2:商品列表(等登录测试完才开始)
const page2 = await browser.newPage();
await page2.goto('https://example.com/products');
// ... 继续等待
}
这段代码的问题显而易见:测试2必须等测试1完全结束。如果测试1中的网络请求慢2秒,所有后续测试都跟着慢。
串行是效率的坟墓,并行是速度的起点。
02 并行化:让Puppeteer同时跑10个测试
核心思路:用多个浏览器上下文(BrowserContext)同时运行测试。每个上下文独立,互不干扰。
实现方案:Promise.all + 动态上下文池。
javascript
const { chromium } = require('puppeteer');
async function runParallelTests(testCases, concurrency = 5) {
const browser = await chromium.launch({ headless: true });
// 创建上下文池
const contexts = [];
for (let i = 0; i < concurrency; i++) {
contexts.push(await browser.createIncognitoBrowserContext());
}
// 任务分配器
const results = [];
let currentIndex = 0;
async function worker(contextId) {
while (currentIndex < testCases.length) {
const testIndex = currentIndex++;
const testCase = testCases[testIndex];
const context = contexts[contextId];
const page = await context.newPage();
try {
console.log(`开始测试 ${testIndex + 1}: ${testCase.name}`);
const startTime = Date.now();
await testCase.run(page);
const duration = Date.now() - startTime;
results.push({ index: testIndex, status: 'pass', duration });
console.log(`测试 ${testIndex + 1} 通过 (${duration}ms)`);
} catch (error) {
results.push({ index: testIndex, status: 'fail', error: error.message });
console.error(`测试 ${testIndex + 1} 失败: ${error.message}`);
} finally {
await page.close();
}
}
}
// 启动并发worker
const workers = [];
for (let i = 0; i < concurrency; i++) {
workers.push(worker(i));
}
await Promise.all(workers);
await browser.close();
return results;
}
// 使用示例
const testCases = [
{ name: '登录测试', run: async (page) => { /* ... */ } },
{ name: '商品搜索', run: async (page) => { /* ... */ } },
{ name: '购物车操作', run: async (page) => { /* ... */ } },
// 50个测试用例
];
const concurrency = 10; // 同时跑10个
const results = await runParallelTests(testCases, concurrency);
效果数据:
- ►50个测试用例,串行执行:平均耗时 250秒
- ►10个并行执行:平均耗时 28秒
- ►速度提升 8.9倍
并行不是魔法,是把等待变成干活的时间。
03 懒加载优化:别让页面等你,让测试主动出击
现代Web应用大量使用懒加载。图片、组件、数据,用户滚动才加载。但E2E测试中,这成了噩梦——测试等待某个元素出现,但页面还没滚动到那个位置。
解决方案:强制触发懒加载 + 智能等待策略
javascript
// 懒加载优化函数
async function optimizeLazyLoading(page, options = {}) {
const {
scrollStep = 500, // 每次滚动像素
scrollDelay = 100, // 滚动间隔ms
waitForNetworkIdle = true // 等待网络空闲
} = options;
// 强制滚动页面,触发所有懒加载
await page.evaluate(async (step, delay) => {
const scrollHeight = document.documentElement.scrollHeight;
const viewportHeight = window.innerHeight;
let scrolled = 0;
while (scrolled < scrollHeight) {
window.scrollBy(0, step);
scrolled += step;
await new Promise(resolve => setTimeout(resolve, delay));
}
// 回到顶部
window.scrollTo(0, 0);
}, scrollStep, scrollDelay);
// 等待所有图片加载完成
await page.evaluate(() => {
return Promise.all(
Array.from(document.images)
.filter(img => !img.complete)
.map(img => new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
}))
);
});
// 等待网络空闲
if (waitForNetworkIdle) {
await page.waitForNetworkIdle({ idleTime: 500 });
}
}
// 在测试中使用
async function testProductPage(page) {
await page.goto('https://example.com/products');
// 优化:强制触发所有懒加载
await optimizeLazyLoading(page, { scrollStep: 300, waitForNetworkIdle: true });
// 现在所有元素都已加载,测试更可靠
const productCount = await page.$$eval('.product-card', cards => cards.length);
console.log(`产品数量: ${productCount}`);
// 测试懒加载图片已加载
const brokenImages = await page.$$eval('img[src]', imgs =>
imgs.filter(img => !img.complete || img.naturalWidth === 0).length
);
console.log(`损坏图片: ${brokenImages}`);
}
优化前后对比:
- ►优化前:等待懒加载超时,测试平均耗时 45秒
- ►优化后:主动触发加载,测试平均耗时 12秒
- ►等待时间减少 73%
别被动等待,主动出击才是测试的生存法则。
04 依赖优化:把登录、初始化做成缓存
每个测试都重新登录?每个测试都重建数据?这是最大的时间浪费。
方案:共享登录状态 + 数据预置
javascript
const { chromium } = require('puppeteer');
class TestManager {
constructor() {
this.browser = null;
this.authState = null; // 缓存登录状态
this.storageState = null; // 缓存storage
}
async init() {
this.browser = await chromium.launch({ headless: true });
}
// 登录并缓存状态
async login(page) {
if (this.authState) {
// 恢复登录状态
await page.evaluateOnNewDocument(() => {
// 设置cookie等
});
return;
}
await page.goto('https://example.com/login');
await page.type('#username', 'testuser');
await page.type('#password', 'password123');
await page.click('#login-btn');
await page.waitForNavigation();
// 保存cookie和storage
const cookies = await page.cookies();
const localStorage = await page.evaluate(() =>
JSON.stringify(window.localStorage)
);
this.authState = { cookies, localStorage };
// 保存到文件,供后续测试使用
const fs = require('fs');
fs.writeFileSync('auth-state.json', JSON.stringify(this.authState));
}
// 快速创建已登录页面
async createLoggedInPage(context) {
const page = await context.newPage();
// 如果已有缓存,直接恢复
if (this.authState) {
await page.setCookie(...this.authState.cookies);
await page.evaluateOnNewDocument((state) => {
const parsed = JSON.parse(state);
Object.keys(parsed.localStorage).forEach(key => {
window.localStorage.setItem(key, parsed.localStorage[key]);
});
}, JSON.stringify(this.authState));
}
return page;
}
// 数据预置:在测试前创建好数据
async setupTestData(page) {
// 批量创建测试商品
await page.goto('https://example.com/admin/products');
for (let i = 0; i < 10; i++) {
await page.click('#add-product');
await page.type('#product-name', `测试商品-${i}`);
await page.type('#product-price', `${(i + 1) * 10}`);
await page.click('#save-product');
await page.waitForSelector('.success-message');
}
}
async destroy() {
if (this.browser) {
await this.browser.close();
}
}
}
// 优化后的测试流程
async function optimizedTestSuite() {
const manager = new TestManager();
await manager.init();
const testCases = [
{ name: '商品列表', run: async (context) => {
const page = await manager.createLoggedInPage(context);
await page.goto('https://example.com/products');
// ... 测试逻辑
}},
{ name: '购物车', run: async (context) => {
const page = await manager.createLoggedInPage(context);
await page.goto('https://example.com/cart');
// ... 测试逻辑
}},
// 50个测试用例,共享登录状态
];
const results = await runParallelTests(testCases, 10);
await manager.destroy();
return results;
}
优化效果:
- ►每个测试节省:登录耗时 3秒 + 数据创建 5秒 = 8秒
- ►50个测试节省:50 × 8 = 400秒
- ►总测试时间:从 650秒 降至 250秒
缓存是程序员最好的朋友,重复是测试最快的杀手。
05 实战:完整优化方案,让测试跑进10分钟
将以上三招组合,形成一套完整的Puppeteer E2E测试优化方案。
最终优化代码:
javascript
const { chromium } = require('puppeteer');
const fs = require('fs');
class OptimizedE2ETester {
constructor(config = {}) {
this.config = {
concurrency: 10,
scrollStep: 300,
scrollDelay: 100,
authFile: 'auth-state.json',
...config
};
this.browser = null;
this.authState = null;
}
async init() {
this.browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
// 尝试加载缓存的登录状态
if (fs.existsSync(this.config.authFile)) {
this.authState = JSON.parse(fs.readFileSync(this.config.authFile, 'utf8'));
console.log('已加载缓存的登录状态');
}
}
async login(page) {
if (this.authState) return;
await page.goto('https://example.com/login', { waitUntil: 'networkidle0' });
await page.type('#username', 'testuser');
await page.type('#password', 'password123');
await page.click('#login-btn');
await page.waitForNavigation({ waitUntil: 'networkidle0' });
const cookies = await page.cookies();
const localStorage = await page.evaluate(() =>
JSON.stringify(window.localStorage)
);
this.authState = { cookies, localStorage };
fs.writeFileSync(this.config.authFile, JSON.stringify(this.authState));
console.log('登录状态已缓存');
}
async optimizePage(page) {
// 1. 恢复登录状态
if (this.authState) {
await page.setCookie(...this.authState.cookies);
}
// 2. 禁用不必要的资源加载
await page.setRequestInterception(true);
page.on('request', (request) => {
const type = request.resourceType();
if (['image', 'stylesheet', 'font', 'media'].includes(type)) {
request.abort(); // 测试不需要图片样式
} else {
request.continue();
}
});
// 3. 优化懒加载
await page.evaluate(async (step, delay) => {
const scrollHeight = document.documentElement.scrollHeight;
let scrolled = 0;
while (scrolled < scrollHeight) {
window.scrollBy(0, step);
scrolled += step;
await new Promise(r => setTimeout(r, delay));
}
window.scrollTo(0, 0);
}, this.config.scrollStep, this.config.scrollDelay);
}
async runTests(testCases) {
const contexts = [];
for (let i = 0; i < this.config.concurrency; i++) {
contexts.push(await this.browser.createIncognitoBrowserContext());
}
const results = [];
let currentIndex = 0;
const startTime = Date.now();
async function worker(contextId) {
while (currentIndex < testCases.length) {
const testIndex = currentIndex++;
const testCase = testCases[testIndex];
const page = await contexts[contextId].newPage();
try {
await this.optimizePage(page);
await this.login(page);
const testStart = Date.now();
await testCase.run(page);
const duration = Date.now() - testStart;
results.push({
name: testCase.name,
status: 'pass',
duration,
timestamp: new Date().toISOString()
});
console.log(`✅ ${testCase.name} (${duration}ms)`);
} catch (error) {
results.push({
name: testCase.name,
status: 'fail',
error: error.message
});
console.error(`❌ ${testCase.name}: ${error.message}`);
// 截图保存失败现场
await page.screenshot({
path: `failure-${testCase.name.replace(/\s+/g, '-')}.png`
});
} finally {
await page.close();
}
}
}
const workers = [];
for (let i = 0; i < this.config.concurrency; i++) {
workers.push(worker.call(this, i));
}
await Promise.all(workers);
const totalTime = Date.now() - startTime;
console.log(`\n
声明:本站部分稿件版权来源于网络,如有侵犯版权,请及时联系我们。