千锋教育-做有情怀、有良心、有品质的职业教育机构

前端测试自动化:使用Puppeteer进行E2E测试_千锋教育

来源:千锋教育
发布时间:2026-05-29 06:10:47

千锋教育品牌logo

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              
声明:本站部分稿件版权来源于网络,如有侵犯版权,请及时联系我们。

学习资源站

  • 免费全套视频教程
  • 企业实战项目源码
  • 大厂笔试真题题库
  • 行业前瞻发展趋势