头图

背景

前端自动化测试在工程化的研发体系中不可或缺。前端领域的自动化测试常被忽略,原因在于人们认为编写测试用例成本高且意义不大,本质是觉得投资回报率低。但当收益大于支出时,引入前端自动化测试是必要的。例如在表单功能从简单到复杂的迭代过程中,手动测试成本会指数级增长且可能无法完成所有测试,此时引入自动化测试能提升效率、保证测试覆盖范围、减少误差和遗漏、实现用例重复使用等。

成本

  1. 初始成本,引入自动化测试框架,首次适配项目的开发成本。
  2. 新用例的编写成本,每次增加新测试用例都需要编写。
  3. 项目处在初期,迭代频率高,或者用例编写不合理时,自动化测试用例可能存在脆弱性,反而降低测试效率。

收益

  1. 手动测试的成本减少,自动化测试用例可取代大部分手动测试。
  2. 整体的测试速度更快,能缩短需求的迭代周期,提升效率。
  3. 测试的覆盖范围更大,新功能上线时能保证所有存量功能被覆盖。
  4. 误差和遗漏更少,避免人工失误和错误。
  5. 用例可重复使用,在项目多个迭代的多个生命周期里保证测试逻辑的高度一致性。

相关概念

总结了一下,前端测试大概分为 4 类:

  1. 静态检查(Static),比如 eslint 和 typescript 所处理的类型或语法错误,这部分一直在使用,基本保证了第三方库升级或小型重构后在解决完报错以后直接使用。这部分也是目前团队里面已经 cover 到的部分,在 commit 和 ci 中都有做静态检查。
  2. 单元测试(Unit) 用来验证独立的代码片段能否正常工作。
  3. 集成测试(Integration) 用来验证多个单元能否一起工作。
  4. 端对端测试(End to End) 模拟真实用户对应用程序操作,又叫做功能性测试。

以上测试的覆盖范围依次减小,并且单元测试和集成测试之间的区别可能很难区分,正如React官网所言:

对组件来说,“单元测试”和“集成测试”之间的差别可能会很模糊。如果你在测试一个表单,用例是否应该也测试表单里的按钮呢?一个按钮组件又需不需要有他自己的测试套件?重构按钮组件是否应该影响表单的测试用例?不同的团队或产品可能会得出不同的答案。

由于静态检查部分已经完成,因此在我们后面的实践里,主要做剩余三个类型的测试:单元测试(工具函数 utils 单测),集成测试(后面都叫他组件测试,通用 UI 组件单测),和E2E 测试。

技术选型

单元测试 Jest vs Vitest

功能特性JestVitest
经大型公司实战检验
模块支持
支持
浏览器 UI
类型测试
基准测试
源代码测试
浏览器模式
快照测试
交互快照测试
代码覆盖率
并发测试
分片支持
多项目运行器
模拟

结论

经过实践,Jest 对 TS 的支持更加繁琐,要配合 babel 之类实现,其次是它对 vite 也不友好,官网中也有说明这点, 所以最终选择了 Vitest。

Jest can be used in projects that use vite to serve source code over native ESM to provide some frontend tooling, vite is an opinionated tool and does offer some out-of-the box workflows. Jest is not fully supported by vite due to how the plugin system from vite works, but there are some working examples for first-class jest integration using vite-jest, since this is not fully supported, you might as well read the limitation of the vite-jest. Refer to the vite guide to get started.

组件测试 Playwright vs RTL

Playwright

Playwright Component testing 相对较新,被标记为 Playwright 的实验性功能,于2022年1.22.0版本发布实施。在此之前,测试人员必须使用其他框架比如 RTL 来测试他们应用的组件。

React Testing Library(RTL)

RTL 是一个专门为测试 React 组件而设计的测试工具,它的解决方案主要是测试时通过像用户使用一样,查找 dom 元素并与其交互的方式然后验证的方式来进行测试的,应避免测试实现细节,避免重构(修改实现但不修改功能)时造成测试用例失效。

结论

我这边是想让依赖的尽可能少且统一,暂定使用 e2e 框架 playwright 来实现。

E2E 测试 Playwright vs Cypress

CypressPlaywright
TypeScript 支持从 4.4.0 版本开始提供,配置简单,自身 API 有较好的 TS 类型支持,自定义 Commands 支持度一般,需自己写.d.ts 文件开箱即用,TS 开发体验极佳
Authentication 鉴权 - 基本用法可通过 UI 或 API 获取鉴权信息通过 UI 或 API 获取鉴权信息
Authentication 鉴权 - 鉴权复用使用 cy.session 抓取页面 session 并缓存,避免重复登录配置 globalSetup,抓取鉴权信息保存到本地复用
Authentication 鉴权 - 角色切换不支持多 tab 同时运行,所有切换在一个 tab 下进行可以在一个用例中,打开多个窗口进行不同角色账号操作,互不影响
Hover 事件支持事件触发模拟,不支持 Hover ,需借助社区库实现完美支持
拖拽满足基本拖拽需求拖拽相关 API 丰富
文件上传、下载可通过库实现上传,官网无上传文档说明提供多种灵活的上传方式
iframe 支持支持,API 易用性一般相关 API 基本与普通页面一致,使用简单且功能不受限
多 Tab 支持不支持多个 Tab 同时运行完美支持
网络请求支持拦截请求前后,发起请求支持拦截并修改请求、代理请求、发起请求,可准确控制监听触发请求
断言内部捆绑 Chai 断言库,提供 automatically retry使用 jest 的 expect,提供特有断言方法和 re-testing 特性
报告与调试 - 报告自身报告格式有限,社区插件提供多样报告;通过查看 video 与页面快照调试,本地可用 cypress open mode 调试内置多种格式报告支持,HTML 报告体验好;通过查看 video 与 trace 调试,在 VS Code 中可用 Playwright Test for VSCode 调试
语法更接近于 JQuery 语法风格,大量链式调用及全局命令注入更接近于现代 JavaScript & TypeScript 语法风格,可选择面向对象或函数式编码风格
并发执行支持官方不提供单个机器中多个浏览器并发运行用例能力,可通过社区插件实现;提供多个机器中并发执行能力,但需配合 Cypress Dashboard 服务在并发执行方面支持度好,可在相同与不同文件、多种浏览器中并发执行
开源支持 - 收费开源免费,DashBoard 服务免费版有功能与使用数量限制,无法私有化部署开源且完全免费
开源支持 - 社区生态有良好插件生态,但部分插件可能未及时维护无插件生态,官方提供稳定可靠的各种能力
持续集成 - 基本使用使用官方提供的 docker 镜像与 github-action,使用 JamesIves/github-pages-deploy-action 部署报告,使用 actions/upload-artifact 归档使用 mcr.microsoft.com/playwright 镜像,使用 JamesIves/github-pages-deploy-action 部署报告,使用 actions/upload-artifact 归档
持续集成 - 并发执行官方提供的并发执行需配合 Cypress dashboard,并发执行时用例拆分策略无法控制通过 matrix strategies 定义变量实现并发执行

总结

经过实践,Cypress 在 API 可读性方面我个人觉得是优于 Playwright 的,不过这个观点仁者见仁智者见智,Cypress 的 api 要更接近于 jquery,并且它不用写 async await,但是它有部分功能需要付费才能使用,而 Playwright 相当于开箱即用了,什么都有,并且性能更优还完全免费。
总体而言 Playwright 是要更优于 Cypress 的方案的,我们也将在项目中采取这种方案。

项目准备

升级 node 版本

前端统一升级 node 版本到 v20.10.0版本,因为 jsdom(在单元测试中需要用到,他能提供真实的浏览器环境,比如在测试一些下载功能跟浏览器相关的功能时是必须的)要求 node 版本不低于 20x。

1.13 更新:暂时不必升级 node 版本,如果遇到 error lru-cache@11.0.2: The engine "node" is incompatible with this module. Expected version "20 || >=22". Got "18.18.0"报错,请参阅此 issue,在 package.jon添加此配置:

"resolutions": {
  "@asamuzakjp/css-color": "^2.8.3-b.1"
}

更新 script 脚本

为本地测试和 CI 做准备,参考:

{
  "script": {
    "test:e2e:ci": "playwright test",
    "test:unit": "vitest",
    "test:unit:ci": "vitest run",
    "coverage": "vitest run --coverage"
  }
}

升级基础设施前端公共库

目前需要升级的有 @core/utils@core/fetch 两个库,最新版本调整了构建产物,修改了 module 配置使 fetch 包构建后产物包含 index.js 以便项目中 vitest 能正确识别。

新增的依赖库

注意:测试相关的依赖基本都是开发依赖,所以安装在 devDependencies 下面:

{
  "@types/jsdom": "^21.1.7",
  "jsdom": "^26.0.0",
  "vitest": "^2.1.8",
  "@playwright/test": "^1.49.1",
  "playwright": "^1.49.1"
}

更新 vite 配置

在原来的 vite.config.js 中新增 test 字段, 其中 test.include 应该根据你实际的目录来决定。

{
  "test": {
    "environment": "jsdom",
    "include": ["./tests/unit/*.test.ts"]
  }
}

新增 playwright 配置文件

这里给一份比较初始的配置,主要就是改了一下指定测试目录和需要在哪些浏览器下测试,具体的配置信息请参阅文档

import { defineConfig, devices } from '@playwright/test'

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  // 测试目录
  testDir: './tests/e2e',
  // 是否并发运行测试
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  // 测试失败用例重试次数
  retries: process.env.CI ? 2 : 0,
  // 测试时使用的进程数,进程数越多可以同时执行的测试任务就越多。不设置则尽可能多地开启进程。
  workers: process.env.CI ? 1 : undefined,
  // 指定测试结果如何输出
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  // 测试 project 的公共配置,会与与下面 projects 字段中的每个对象的 use 对象合并。
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    // 测试时各种请求的基础路径
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://127.0.0.1:3000',

    // 生成测试追踪信息的规则,on-first-retry 意为第一次重试时生成。
    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry'
  },
  // 定义每个 project,示例中将不同的浏览器测试区分成了不同的项目
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    }
  ]
})

风格

tests/
├── component/ # 组件测试目录
│ ├── Address.spec.tsx # 组件测试文件示例
│ └── stories/ # 组件目录
│     └── Address.stories.tsx # 组件文件示例
│
├── e2e/ # 端到端测试目录
│ └── login.spec.ts # E2E测试文件示例
│
├── playwright/ # Playwright 配置目录
│ └── index.tsx # 测试模板文件
│
├── unit/ # 单元测试目录
│ └── array.test.ts # 单元测试文件示例
│
├── playwright-ct.config.ts # Playwright 组件测试配置文件
└── playwright.config.ts # Playwright 端到端测试配置文件

每个目录的主要用途:

  • component/: 存放组件测试文件,使用 Playwright 进行组件测试
  • component/stories/: 存放组件故事文件,用于组件测试场景定义
  • playwright/: 存放 Playwright 相关配置和模板
  • unit/: 存放普通单元测试文件

MangoGoing
785 声望1.2k 粉丝

开源项目:详见个人详情