前端E2E测试既实用,也简单

534 阅读9分钟

背景

公司近期正在大力推进代码质量管理,要求前端项目也需覆盖一定比例的单元测试。作为试点,我选取了一个 Vue3 项目,开始编写相关的单元测试用例。测试过程中发现,对于纯 JavaScript 工具函数这类纯逻辑模块,单元测试确实效果显著,能够帮助及时发现边界场景和潜在 bug,提升代码鲁棒性。

然而,在尝试为 UI 页面或组件逻辑编写单元测试时,收效却并不明显。由于大多数真实依赖(如路由、接口请求、第三方 UI 组件)都需要 mock 替代,这使得测试环境与真实运行环境存在较大差异,导致测试用例的覆盖深度和准确性受到较大限制。比如一个典型的课程列表页面,我们希望验证接口返回的数据能否正确渲染在页面中。如果接口数据是 mock 的,即便页面渲染逻辑出现了问题或业务逻辑被修改,只要 mock 数据结构没变,测试用例依然可能通过。这在一定程度上削弱了测试的有效性和实际意义。使用单元测试去验证 UI 页面或组件逻辑更多是形式上的覆盖,难以真正还原复杂的业务场景,其带来的质量提升也较为有限。

因此,我认为在当前前端项目中,UI 层的逻辑和行为更适合通过端到端(E2E)测试进行验证。与单元测试不同,E2E 测试是在尽可能还原真实使用场景的前提下,从用户的视角出发,通过自动化脚本完整地走一遍功能流程,以验证系统的整体协作是否符合预期。

比如课程列表页面的测试,我们可以通过 E2E 脚本打开页面、模拟用户操作,等待接口真实返回后,直接校验页面是否正确渲染关键内容。这种方式不再依赖大量 mock,而是尽量模拟用户实际的使用路径,对组件的交互行为、异步数据加载、页面跳转逻辑等都能进行更真实有效的验证,从而真正守住产品质量的“最后一道防线”。

对于偏重交互逻辑、异步渲染和多个模块协同的 UI 页面,单靠单元测试并不能提供足够的信心保障。引入 E2E 测试,不仅提升了覆盖的真实度,也有助于开发、测试和产品多方在验收逻辑时达成一致预期。

E2E测试方案选择

项目采用的是Vue3+Vite+Pnpm+Jenkins技术方案,我调研了三种E2E测试方案:Cypress,Playwright, Nightwatch。Nightwatch是第一个出局的。

Nightwatch的出局理由

1. 历史包袱与社区关注度下降

  • Nightwatch 曾是 Selenium 时代 E2E 测试的先锋,但随着 Playwright 和 Cypress 的崛起,它的社区关注度逐渐被分流。
  • 更新节奏较慢,生态活跃度偏低,很多问题要靠文档 + GitHub Issues 自助解决。

2. CI/CD 适配性不如 Playwright

  • 在 Jenkins 这样的 CI 平台中,Nightwatch 通常仍需要手动配置 webdriver 或 chromedriver 的路径,对 Linux Headless 环境不够“零配置友好 。
  • Playwright 一条命令就能在无头模式下装好依赖,Nightwatch 则可能需要配置 xvfb 或显示环境模拟。

3. 调试与可视化体验较弱

  • 缺少像 Cypress 那样的 GUI 调试器,也没有 Playwright 的 VSCode 插件或 codegen 录制能力。
  • 开发过程中如果出现 flaky 测试,定位问题会更加费时。

4. Vue 3 生态适配并不主流

  • 虽然支持 Vue 组件测试(通过 @nightwatch/vuevite-plugin-nightwatch),但文档、教程数量明显比 Playwright 和 Cypress 少。
  • 一些高级功能(比如模拟 Router、Pinia 状态)仍然偏向自定义和繁琐。

Cypress的出局理由

在Cypress和Playwright之间,我权衡了好久,对比来对比去,最终选择了Playwright。

  • Cypress vs Playwright 对比速览
特性CypressPlaywright
定位专注前端交互测试更广泛的 E2E 自动化解决方案(包括多浏览器)
浏览器支持Chromium(默认)、Firefox、EdgeChromium、Firefox、WebKit(含 Safari)
测试稳定性强依赖浏览器内部机制(Runs in-browser)在 Node 中执行,支持更底层的控制
调试体验超强 GUI 界面,所见即所得CLI 和 VSCode 插件,调试功能逐步完善
速度中等更快,支持并发执行
CI/CD 友好度良好极佳,原生无头模式、并发执行
API 灵活性简洁易学(略微限制)高度灵活(功能强大)
网络拦截与 mock支持但相对有限网络层控制非常强,可模拟几乎一切请求
学习曲线更平缓,文档易读稍陡,但上手也不难
  • Cypress vs Playwright 在 Jenkins 中的表现
对比维度CypressPlaywright
安装依赖简单,需安装 Chrome 或 Electron简单,需安装 Node 支持即可
Jenkins 环境依赖需要 GUI 支持(或用 xvfb)原生支持无头模式,无需 GUI 环境
执行效率较慢,默认单线程支持多线程并发测试(提高速度)
报告集成支持 mochawesome、allure 报告等同样支持 allure,生成结构更丰富
测试稳定性有时遇到 flaky 测试(需显式等待)控制更底层,等待机制稳定
适配 Jenkins Agent略微挑剔(GUI、Chrome等)更通用,适配 headless 环境更自然

尽管 Cypress 在“本地调试体验”上几乎是无敌的(所见即所得、UI 漂亮、开发者友好),但:

  1. 项目部署使用的是 Jenkins: 在无头、Docker、云服务器等 CI 环境中,Cypress 的 GUI 依赖是一种负担。虽然可以用 xvfb 等解决方案,但本质上还是“为适配 CI 而生”的 Playwright 更自然。
  2. Vite + pnpm 组合强调速度和效率: 而 Cypress 的执行模型本身是单线程,加载和执行 E2E 用例时,容易形成瓶颈。Playwright 原生支持并发 runner,更适合这种构建速度导向的现代前端。
  3. 复杂交互的能力局限: 如果要测试登录流程、OAuth 重定向、多页面嵌套,Playwright 在“脚本灵活性”和“控制浏览器底层行为”方面表现明显更强。
  4. 可扩展性: Playwright 还能轻松用于测试 Electron 应用、移动模拟器、API 请求,具有更高的拓展空间。

如何用Playwright做E2E测试?

step1 安装依赖

pnpm add -D @playwright/test
pnpm exec playwright install

pnpm exec playwright install命令会安装好几种无头浏览器,如果不需要那么多

image.png

只想安装chromium,执行下面这句:

npx playwright install chromium

step2 创建playwriht.config.ts配置文件

新建一个名为playwriht.config.ts的文件,配置如下

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e', // 测试用例文件夹
  timeout: 300 * 1000, // 每个测试最大超时
  use: {
    baseURL: 'http://localhost:5173',
    browserName: 'chromium',
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  webServer: {
    command: 'vite preview --port 5173',
    port: 5173,
    reuseExistingServer: !process.env.CI,
  },
  reporter: [['html', { open: 'never' }]],
});

可以根据实际需要把 testDir 改成你存放测试脚本的目录,比如 e2e/specs/ 等。

step3 在package.json添加测试指令

{
  "scripts": {
    "dev": "vite",
    "test:e2e": "playwright test",
    "test:e2e:report": "playwright show-report"
  }
}

step4 编写e2e测试用例

在项目根目录下创建tests/login.spec.ts,编写登录页面 E2E 测试用例

import { test, expect } from '@playwright/test';

test('用户登录成功跳转到首页', async ({ page }) => {
  // dev线上登录
  await page.goto('https://dev.example.com/cas/login/#/login?appId=xxx');

  // 填写账号密码(用测试账号)
  await page.fill('input[id="credential"]', '账户');
  await page.fill('input[id="secret"]', '密码');
  await page.click('div.btn:has-text("登录")');

  // 登录成功后,初始 URL 为 /#/?tk=xxx,然后再 JS 重定向或路由跳转到 #/model,需要等待一段时间
  await page.waitForFunction(() => location.hash.includes('model'), { timeout: 8000 });
  await expect(page).toHaveURL(/\/data\/ai-platform-frontend\/#\/model/);

  // 验证用户名是否正确显示
  await expect(page.locator('.layout-header')).toContainText('李二');
});


运行pnpm test:e2e运行测试, 测试成功之后,运行pnpm test:e2e:report查看测试报告

  • 成功测试报告:

image.png

  • 失败测试报告

失败时,有截图和录屏,可以查看具体卡在了哪里。

image.png

image.png

step5 Jenkinsfile e2e 流程

1. 制作playwright基础镜像 为什么要先制作基础镜像呢? 是因为在Jenkins CI机器上,执行之后

npx playwright install chromium

依旧提示playwright有一大堆依赖需要安装,而且需要管理者权限

image.png

不如制作一个基础playwright镜像,用起来更省心,更稳定

首先在Dockerfile文件中定义镜像制作流程, 注意这里的playwright:v1.54.1版本要与项目中的 "@playwright/test": "1.54.1"保持一致

FROM mcr.microsoft.com/playwright:v1.54.1-jammy

# 设置环境变量,让 Playwright 在运行时找到浏览器
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

RUN npx playwright install chromium

接着定义一个Jenkins Job任务,执行镜像生成逻辑

pipeline {
  agent any

  environment {
    REGISTRY_HOST = 'reg.xxx.com:9088'
  }

  stages {
    stage('制作PlayWright镜像') {
      steps {
        script {
          try {
            updateGitlabCommitStatus(name: '生成PlayWright镜像', state: 'pending')
            withCredentials([
              usernamePassword(
                  credentialsId: 'REGISTRY',
                  usernameVariable: 'REGISTRY_USERNAME',
                  passwordVariable: 'REGISTRY_PASSWORD'
              )
            ]) {
              dir('common-tools/playwright-image') {
                sh """
                    echo "\$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" -u "\$REGISTRY_USERNAME" --password-stdin
                    docker build -t $REGISTRY_HOST/base-images/playwright:v1.54.1 .
                    docker push $REGISTRY_HOST/base-images/playwright:v1.54.1
                """
              }
            }

            updateGitlabCommitStatus(name: '生成PlayWright镜像', state: 'success')
          } catch (Exception e) {
            updateGitlabCommitStatus(name: '生成PlayWright镜像', state: 'failed')
            throw e
          }
        }
      }
    }
  }
}

pipeline {
  agent any

  stages {
    stage('检出代码') {
      steps {
        checkout scm
      }
    }
    
    stage('安装依赖') {
      steps {
        sh 'pnpm i'
      }
    }

    stage('运行 E2E 测试') {
      steps {
       docker.image("${env.REGISTRY_HOST}/base-images/playwright:v1.54.1").inside('-v $PWD:/e2e -w /e2e') {
                sh '''
                  mkdir -p /home/jenkins/.cache
                  ln -s /ms-playwright /home/jenkins/.cache/ms-playwright
                  pnpm test:e2e
                '''
         }
      }
    }

    stage('生成报告') {
      steps {
        sh 'pnpm test:e2e:report'
      }
    }
  }

  post {
    always {
      // 将测试报告归档
      archiveArtifacts artifacts: '**/playwright-report/**', allowEmptyArchive: true
    }
  }
}

最后

在现代前端开发中,测试策略从不只是代码覆盖率的游戏。尤其是在 UI 页面和组件交互丰富的场景中,端到端(E2E)测试更贴近“用户真实使用的角度 ,比单元测试更能暴露问题、保障体验。

尽管单元测试可以快速验证某个函数或组件在某种输入下是否返回预期输出,但它往往忽略了跨组件协同、路由跳转、DOM 渲染顺序、浏览器兼容性、真实 API 行为等维度。换句话说,单元测试关注的是代码是否能跑,而 E2E 测试关注的是用户是否能用。

以登录流程为例,单元测试也许能验证按钮是否存在、方法是否被调用,但无法验证登录按钮点击后,是否真的跳转、是否写入 cookie、是否显示用户信息……而这些,恰恰才是用户真正关心的结果。

因此,在 UI 页面和组件测试中,E2E 测试不仅仅是补充单测的工具,更是保障系统端到端完整性的关键一环。它模拟的是“人和浏览器之间的真实协作” ,而不是脱离上下文的函数行为。这种“以用户为中心”的测试方式,不仅提升了系统的健壮性,也让团队可以更有信心地交付每一个前端功能。个人认为,前端测试应该以E2E测试为主, 单元测试为辅。给各位掘友留个彩蛋,这是一个非常提效编写测试用例的命令,感兴趣可以研究下。

npx playwright codegen https://your.site.domain