Puppeteer 如何与测试框架集成实现 E2E 和 CI/CD?
Puppeteer 可以与 Jest、Mocha、Vitest 等主流测试框架深度集成,完成端到端测试、视觉回归测试和性能测试,再通过 GitHub Actions、Docker 等工具接入 CI/CD 流水线。以下是生产环境中经过验证的集成方式和最佳实践。
与 Jest 集成
Jest 是与 Puppeteer 搭配最多的测试框架,jest-puppeteer 提供了开箱即用的预设配置。
安装核心依赖:
bashnpm install --save-dev puppeteer jest jest-puppeteer @types/puppeteer
jest-puppeteer 的配置文件:
javascript// jest-puppeteer.config.js module.exports = { launch: { headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'], }, browserContext: 'incognito', exitOnPageError: true, };
Jest 配置文件:
javascript// jest.config.js module.exports = { preset: 'jest-puppeteer', testMatch: ['**/e2e/**/*.test.js'], setupFilesAfterEnv: ['./e2e/setup.js'], testTimeout: 30000, };
setup 文件中处理每个测试的前置条件:
javascript// e2e/setup.js beforeEach(async () => { await page.setViewport({ width: 1280, height: 720 }); await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' }); });
编写一个完整的登录 E2E 测试:
javascript// e2e/auth.test.js describe('用户登录流程', () => { test('使用正确的凭据登录成功', async () => { await page.type('[data-testid="username"]', 'testuser'); await page.type('[data-testid="password"]', 'password123'); await page.click('[data-testid="login-btn"]'); await page.waitForSelector('[data-testid="dashboard"]'); const welcome = await page.$eval( '[data-testid="welcome-msg"]', el => el.textContent ); expect(welcome).toContain('欢迎回来'); }); test('使用错误密码登录失败', async () => { await page.type('[data-testid="username"]', 'testuser'); await page.type('[data-testid="password"]', 'wrongpassword'); await page.click('[data-testid="login-btn"]'); await page.waitForSelector('[data-testid="error-msg"]'); const error = await page.$eval( '[data-testid="error-msg"]', el => el.textContent ); expect(error).toContain('用户名或密码错误'); }); });
注意这里使用 data-testid 选择器而非 CSS 类名或 ID,这能让测试不依赖 UI 样式变更,提升稳定性。
与 Mocha 集成
Mocha 的灵活性更高,适合需要自定义测试生命周期的团队。Puppeteer 的浏览器生命周期需要手动管理。
javascript// test/setup.js const puppeteer = require('puppeteer'); const { expect } = require('chai'); let browser; before(async () => { browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'], }); }); after(async () => { await browser.close(); }); beforeEach(async function () { this.page = await browser.newPage(); await this.page.goto('http://localhost:3000'); }); afterEach(async function () { await this.page.close(); });
Mocha 测试用例:
javascript// test/user.spec.js describe('用户注册功能', function () { this.timeout(15000); it('填写完整信息后注册成功', async function () { const { page } = this; await page.click('[data-testid="register-link"]'); await page.type('[data-testid="reg-username"]', 'newuser'); await page.type('[data-testid="reg-email"]', 'new@example.com'); await page.type('[data-testid="reg-password"]', 'StrongP@ss1'); await page.click('[data-testid="reg-submit"]'); await page.waitForSelector('[data-testid="reg-success"]'); const text = await page.$eval( '[data-testid="reg-success"]', el => el.textContent ); expect(text).to.include('注册成功'); }); });
Mocha 的 this.timeout() 需要显式设置,Puppeteer 测试通常需要 10-30 秒的超时时间,不要使用箭头函数,否则无法访问 Mocha 的 this 上下文。
Page Object 模式
无论使用 Jest 还是 Mocha,当测试用例超过 20 个时,必须引入 Page Object 模式。它把页面元素定位和操作封装成独立类,测试用例只关心业务逻辑。
javascript// pages/LoginPage.js class LoginPage { constructor(page) { this.page = page; this.usernameInput = '[data-testid="username"]'; this.passwordInput = '[data-testid="password"]'; this.submitBtn = '[data-testid="login-btn"]'; this.errorMessage = '[data-testid="error-msg"]'; } async login(username, password) { await this.page.type(this.usernameInput, username); await this.page.type(this.passwordInput, password); await this.page.click(this.submitBtn); } async getErrorMessage() { await this.page.waitForSelector(this.errorMessage); return this.page.$eval(this.errorMessage, el => el.textContent); } } module.exports = LoginPage;
测试用例中使用 Page Object:
javascriptconst LoginPage = require('../pages/LoginPage'); test('登录失败显示错误提示', async () => { const loginPage = new LoginPage(page); await loginPage.login('testuser', 'wrongpass'); const error = await loginPage.getErrorMessage(); expect(error).toContain('用户名或密码错误'); });
如果 UI 改了 data-testid 的值,只需修改 LoginPage 一处,所有引用它的测试自动更新。这就是 Page Object 的核心价值。
视觉回归测试
视觉回归测试能捕获 CSS 改动导致的 UI 偏移,Puppeteer 结合 Percy 或 jest-image-snapshot 可以自动完成截图对比。
使用 jest-image-snapshot 的方式:
bashnpm install --save-dev jest-image-snapshot
javascriptconst { toMatchImageSnapshot } = require('jest-image-snapshot'); expect.extend({ toMatchImageSnapshot }); test('首页视觉一致性', async () => { await page.goto('http://localhost:3000'); await page.waitForSelector('#main-content'); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchImageSnapshot({ failureThreshold: 0.03, failureThresholdType: 'percent', }); });
failureThreshold 设为 3% 是比较合理的起点,太严格会导致大量误报,太宽松又漏掉真正的 UI 变化。首次运行会生成基准截图,后续运行自动对比。
性能测试
Puppeteer 可以通过 Chrome DevTools Protocol 采集性能指标,结合 Lighthouse 做更全面的审计。
javascriptconst puppeteer = require('puppeteer'); test('首页核心性能指标', async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' }); // 采集 Core Web Vitals const metrics = await page.evaluate(() => { return new Promise(resolve => { new PerformanceObserver(list => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; resolve({ LCP: lastEntry.startTime, FID: 0, CLS: 0, }); }).observe({ type: 'largest-contentful-paint', buffered: true }); }); }); expect(metrics.LCP).toBeLessThan(2500); await browser.close(); });
性能测试中 waitUntil: 'networkidle0' 很关键,确保页面资源加载完成后再采集数据。
CI/CD 集成
CI/CD 是 Puppeteer 测试从本地走向生产的关键环节。主要解决三个问题:浏览器安装、无头模式运行、测试稳定性。
GitHub Actions 配置:
yamlname: E2E Tests on: [push, pull_request] jobs: e2e: runs-on: ubuntu-latest container: image: node:18-slim steps: - uses: actions/checkout@v4 - name: Install Chrome dependencies run: | apt-get update apt-get install -y chromium libx11-xcb1 libxcomposite1 libxdamage1 libxi6 libxtst6 libnss3 libatk1.0-0 - name: Install dependencies run: npm ci - name: Run E2E tests run: npm run test:e2e env: CI: true PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
Docker 配置:
dockerfileFROM node:18-slim RUN apt-get update && apt-get install -y \ chromium \ --no-install-recommends && \ rm -rf /var/lib/apt/lists/* ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . CMD ["npm", "run", "test:e2e"]
Docker 环境下必须设置 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true,使用系统安装的 Chromium,避免 Puppeteer 自带浏览器在容器中启动失败。--no-sandbox 参数在 Docker 中也是必需的,因为容器默认以 root 运行,Chrome 要求沙箱模式下不能是 root。
测试稳定性实践
Puppeteer 测试在 CI 环境中失败率较高,以下是提升稳定性的关键手段。
等待策略:永远不要使用 waitForTimeout,改用显式等待。
javascript// 错误做法:硬编码等待 await page.waitForTimeout(3000); // 正确做法:等待元素可见 await page.waitForSelector('[data-testid="result"]', { visible: true }); // 等待网络空闲 await page.waitForNavigation({ waitUntil: 'networkidle2' }); // 等待特定请求完成 await page.waitForResponse( resp => resp.url().includes('/api/user') && resp.status() === 200 );
测试隔离:每个测试用例使用独立的浏览器上下文,避免 Cookie 和 Storage 污染。
javascriptbeforeEach(async () => { context = await browser.createIncognitoBrowserContext(); page = await context.newPage(); await page.goto('http://localhost:3000'); }); afterEach(async () => { await context.close(); });
失败截图:测试失败时自动保存截图,方便排查 CI 中的问题。
javascriptafterEach(async function () { if (this.currentTest.state === 'failed') { const timestamp = Date.now(); const testName = this.currentTest.title.replace(/\s+/g, '_'); await page.screenshot({ path: `screenshots/${testName}_${timestamp}.png`, fullPage: true, }); } });
重试机制:CI 环境中网络和资源加载不稳定,给 E2E 测试加一层重试。
javascript// jest.config.js module.exports = { preset: 'jest-puppeteer', retryTimes: 2, };
测试分层与并行执行
当测试规模增长后,需要按速度和稳定性分层运行,并利用并行加速。
javascript// jest.config.js module.exports = { preset: 'jest-puppeteer', projects: [ { displayName: 'smoke', testMatch: ['**/e2e/smoke/**/*.test.js'], retryTimes: 1, }, { displayName: 'critical', testMatch: ['**/e2e/critical/**/*.test.js'], retryTimes: 2, }, { displayName: 'full', testMatch: ['**/e2e/full/**/*.test.js'], retryTimes: 2, maxWorkers: 4, }, ], };
smoke 测试只覆盖核心路径(登录、关键业务流程),在每次提交时运行;critical 测试覆盖主要功能,在 PR 合并时运行;full 测试覆盖所有场景,在每日构建时运行。
并行执行时注意 maxWorkers 不要超过 CPU 核心数,每个 worker 会启动一个浏览器实例,过度并行反而会因资源争抢导致测试超时。
从 Puppeteer 迁移到 Playwright 的考虑
如果团队需要跨浏览器测试(Firefox、Safari),或者对自动等待、网络拦截有更高要求,Playwright 是更合适的选择。Playwright 由微软维护,API 设计参考了 Puppeteer 并做了大量改进。
迁移路径:Puppeteer 的 page 对象与 Playwright 的 page 对象 API 相似但不完全兼容。最稳妥的方式是先保留 Puppeteer 的集成测试,新测试用 Playwright 编写,逐步替换。不要一次性迁移,风险太大。
如果你的项目只需要 Chrome 测试,Puppeteer 仍然是最轻量的选择。