WinddSnow

Frontend-Testing-Basics-Jest-Unit-Component-E2E

字数统计: 3.9k阅读时长: 16 min
2026/07/31

第86课:前端测试基础——Jest、单元测试、组件测试、E2E 测试简介

测试是保障软件质量和可维护性的基石。前端测试体系通常划分为三个层次:单元测试(验证函数、工具方法等最小可测试单元的正确性)、组件测试(验证 UI 组件在给定 Props 和状态下渲染及交互的正确性)、端到端测试(E2E)(模拟真实用户操作,验证整个应用流程在多页面、多系统间的完整性)。本节课将聚焦最流行的测试工具 Jest——一套零配置、快照测试、覆盖率报告集于一体的 JavaScript 测试框架——并分别演示单元测试、React/Vue 组件测试的编写方式,最后概述 E2E 测试工具(Cypress/Playwright)的核心概念和使用场景。


1. Jest 核心概念与快速开始

Jest 是 Facebook 开源的 JavaScript 测试框架,内建了断言库模拟函数快照测试代码覆盖率报告,无需额外安装。它通过 jest 命令运行所有符合条件的测试文件。

1.1 安装与配置

1
npm install -D jest

package.json 中添加 test 脚本:

1
2
3
4
5
{
"scripts": {
"test": "jest"
}
}

Jest 默认查找 __tests__ 目录下的所有文件,以及任何以 .test.js.spec.js 结尾的文件。可以通过 jest.config.js 自定义匹配规则。

1.2 第一个测试用例

1
2
3
4
5
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
1
2
3
4
5
6
// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

运行 npm test,Jest 会执行测试并输出结果。

关键 API

  • test(name, fn) / it(name, fn):定义一个测试用例。
  • expect(value):包装待测值,返回断言对象。
  • 匹配器(Matchers).toBe(value)(严格相等)、.toEqual(value)(深度相等)、.toBeTruthy().toBeFalsy().toContain(item).toThrow(error?) 等。

1.3 分组与生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
describe('Array operations', () => {
let arr;

beforeEach(() => {
arr = [1, 2, 3]; // 每个测试前重新初始化
});

afterEach(() => {
arr = []; // 每个测试后清理
});

test('push adds an element', () => {
arr.push(4);
expect(arr).toEqual([1, 2, 3, 4]);
});

test('pop removes the last element', () => {
arr.pop();
expect(arr).toHaveLength(2);
});
});

describe 用于组织测试套件,beforeEach/afterEach/beforeAll/afterAll 管理测试前后的准备和清理工作。


2. 单元测试:测试函数与工具方法

单元测试关注最小的可测试逻辑单元——通常是一个函数。目标是在隔离的环境中验证每个函数在各种输入下是否返回预期结果,包括边界情况和异常路径。

2.1 测试纯函数

1
2
3
4
5
// mathUtils.js
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mathUtils.test.js
const { divide } = require('./mathUtils');

describe('divide', () => {
test('divides two positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});

test('divides negative numbers', () => {
expect(divide(-6, 3)).toBe(-2);
});

test('throws on division by zero', () => {
expect(() => divide(5, 0)).toThrow('Division by zero');
});
});

2.2 测试异步代码

Jest 支持 Promise 和 async/await 风格的异步测试。

1
2
3
4
5
6
// fetchData.js
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) throw new Error('Network error');
return response.json();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// fetchData.test.js
const { fetchData } = require('./fetchData');

global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Alice' }),
})
);

test('fetchData returns parsed JSON', async () => {
const data = await fetchData('/api/user');
expect(data).toEqual({ id: 1, name: 'Alice' });
expect(fetch).toHaveBeenCalledWith('/api/user');
});

Mock(模拟) 是单元测试的核心技巧:通过 jest.fn() 创建模拟函数,替代真实的 fetch、数据库查询或第三方 API,使测试快速、稳定且无外部依赖。

2.3 模拟模块

1
2
3
4
5
6
7
8
jest.mock('axios');
const axios = require('axios');

test('uses mocked axios', async () => {
axios.get.mockResolvedValue({ data: { success: true } });
const result = await axios.get('/api');
expect(result.data.success).toBe(true);
});

jest.mock('moduleName') 自动将整个模块替换为模拟版本,结合 mockResolvedValue/mockRejectedValue 控制返回值。


3. 组件测试:验证 UI 行为

组件测试(Component Testing)介于单元测试和 E2E 之间——它渲染一个独立的 UI 组件(而非整个页面),模拟用户交互(点击、输入),断言渲染输出和事件回调是否正确。React 和 Vue 生态各自拥有成熟的组件测试工具。

3.1 React 组件测试(React Testing Library)

React Testing Library 的核心哲学是像用户一样测试——不直接测试组件内部状态或方法,而是通过 DOM 查询和交互来验证行为。它通常与 Jest 配合使用。

安装

1
npm install -D @testing-library/react @testing-library/jest-dom

测试示例

1
2
3
4
5
6
7
8
9
10
11
12
// Counter.jsx
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Counter from './Counter';

test('increments count on button click', () => {
render(<Counter />);
const countElement = screen.getByTestId('count');
const button = screen.getByRole('button', { name: /increment/i });

expect(countElement).toHaveTextContent('Count: 0');
fireEvent.click(button);
expect(countElement).toHaveTextContent('Count: 1');
});

常用查询方法getByTextgetByRolegetByLabelTextgetByTestId(最后的手段)。优先使用 getByRolegetByLabelText,因为它们最接近用户的实际感知方式。

3.2 Vue 组件测试(Vue Test Utils + Testing Library)

Vue 官方推荐 Vue Test Utils(底层 API)或 Vue Testing Library(基于 Testing Library 的封装)。

安装

1
npm install -D @vue/test-utils @testing-library/vue

示例(Vue Test Utils)

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Counter.vue -->
<template>
<div>
<p data-testid="count">Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
// Counter.test.js
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

test('increments count on button click', async () => {
const wrapper = mount(Counter);
const button = wrapper.find('button');
const countP = wrapper.find('[data-testid="count"]');

expect(countP.text()).toContain('0');
await button.trigger('click');
expect(countP.text()).toContain('1');
});

mount 创建组件的完整挂载实例,wrapper.find 查找子元素,trigger 触发事件(返回 Promise 以等待 Vue 的异步更新)。


4. 快照测试(Snapshot Testing)

快照测试是 Jest 特有的功能:它将组件的渲染输出(或任何可序列化的数据结构)保存为一个文本文件(.snap),后续测试时与快照文件进行比较——如果不一致,测试失败,提示差异。这适合用于确保 UI 组件的渲染结果不因意外变更而改变。

1
2
3
4
5
6
7
8
// React 组件快照测试
import renderer from 'react-test-renderer';
import Button from './Button';

test('Button renders correctly', () => {
const tree = renderer.create(<Button label="Click me" />).toJSON();
expect(tree).toMatchSnapshot();
});

注意事项

  • 快照文件应提交到 Git 仓库,作为预期的视觉输出参考。
  • 不应将快照测试视为万能的安全网——它无法验证组件是否“正确”,只能验证“未变”。
  • 更新快照使用 jest --updateSnapshot

5. 代码覆盖率

Jest 内置了覆盖率报告生成能力,运行 npm test -- --coverage 即可输出项目语句、分支、函数和行覆盖率。

覆盖率阈值(可在 jest.config.js 中设置):

1
2
3
4
5
6
7
8
9
10
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

覆盖率指标是质量的参考,但不应追求 100%——核心业务逻辑和复杂分支应被覆盖,而简单的 getter/setter 可不予强求。


6. E2E 测试概述:Cypress 与 Playwright

端到端测试模拟真实用户在浏览器中的操作(点击、输入、导航),验证整个应用从前端到后端数据库的完整工作流。E2E 测试速度较慢,通常仅覆盖关键业务路径(如注册、登录、下单)。

6.1 Cypress

Cypress 是最易上手的 E2E 测试框架,提供实时重加载、时间旅行调试和自动等待功能。它运行在浏览器内部,直接控制 DOM 和网络请求。

1
2
3
4
5
6
7
8
9
10
11
// cypress/e2e/login.cy.js
describe('Login', () => {
it('should log in with valid credentials', () => {
cy.visit('/login');
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome, User').should('be.visible');
});
});

6.2 Playwright

Playwright 由微软开发,支持跨浏览器(Chromium、Firefox、WebKit)测试,提供更强大的网络拦截、多页面、移动设备模拟等能力。

1
2
3
4
5
6
7
8
9
10
11
// tests/login.spec.js
const { test, expect } = require('@playwright/test');

test('should log in with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('text=Welcome, User')).toBeVisible();
});

6.3 测试金字塔

测试类型 数量 速度 成本 关注范围
单元测试 极快 函数、工具方法。
组件测试 独立 UI 组件的交互。
E2E 测试 完整用户流程。

策略:大量单元测试覆盖核心逻辑,适量组件测试确保 UI 行为,少数 E2E 测试保证关键路径畅通。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Jest 中,使用哪个函数来定义一个测试用例?
    A. describe
    B. it(或 test
    C. expect
    D. beforeEach

  2. (单选) React Testing Library 的核心理念是什么?
    A. 测试组件的内部状态和实现细节。
    B. 像用户一样测试组件,通过 DOM 查询和交互验证行为。
    C. 仅支持快照测试。
    D. 需要与 Enzyme 搭配使用。

  3. (填空) 在 Jest 中,要模拟一个模块,可以使用 ______ 函数自动替换整个模块为模拟版本。

  4. (多选) 以下哪些属于 E2E 测试工具?
    A. Jest
    B. Cypress
    C. Playwright
    D. React Testing Library

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为一个工具函数库编写完整的 Jest 单元测试。该工具库包含三个函数:

  • capitalize(str):返回首字母大写的字符串(空字符串返回空字符串)。
  • range(start, end, step = 1):返回一个数组,包含从 start(包含)到 end(不包含)的数字,步长为 step。如果参数不合法(如 step=0 或 start>=end 且 step>0),返回空数组。
  • groupBy(arr, key):接收对象数组和一个键名,返回按该键值分组的 Record 对象。

请为这三个函数编写全面的测试用例,包括正常情况、边界情况和异常情况。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成包含上述三个函数以及它们完整测试套件的 JavaScript 代码。提示词中需明确指定函数实现细节、测试用例覆盖的场景,以及 Jest 的 describe/test/expect 使用方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 JavaScript 工具函数模块 src/utils.js 和其对应的测试文件 src/__tests__/utils.test.js。需要实现:

  1. capitalize(str):若 typeof str !== 'string' 抛出 TypeError;若 str === '' 返回 '';否则返回首字符大写、其余小写的字符串。
  2. range(start, end, step = 1):参数均为数字,若 step === 0(step > 0 && start >= end)(step < 0 && start <= end) 返回 [];否则返回包含从 start(包含)到 end(不包含)步长为 step 的数字数组。
  3. groupBy(arr, key):参数 arr 必须为数组,key 必须为字符串;若数组为空返回 {};否则返回 Record<string, arr元素[]>,按每个元素的 key 值分组(值强制转为字符串作为键)。若元素没有该 key,跳过该元素。
  4. 在测试文件中,使用 describe 对每个函数分组,覆盖正常值、边界值(空字符串、空数组、step 为负、不合法参数等)、异常输入,使用 expect 进行断言。确保测试文件可通过 jest 运行。使用 CommonJS 模块格式。完成后列出两个文件的内容。

四、面试真题与参考答案

题目(腾讯前端面试题):

请解释前端测试金字塔的概念(单元测试、组件测试、E2E 测试),以及它们的适用场景和数量配比。你如何在项目中决定是否为一个新功能编写 E2E 测试?Jest 的模拟(mock)在单元测试中扮演什么角色?

参考答案

测试金字塔是一个指导测试策略的模型:底层是大量快速、廉价的单元测试,中间是适量组件/集成测试,顶层是少量慢速、昂贵的端到端测试。单元测试验证独立函数或工具的纯逻辑;组件测试验证 UI 组件在隔离环境下的渲染和交互;E2E 测试模拟真实用户操作,验证整个应用前后端集成后的完整流程。

决定编写 E2E 测试的依据是该功能是否处于关键业务路径上——如用户注册、登录、核心交易流程、支付等。非关键路径或变化频繁的 UI 细节应更多依赖组件测试和单元测试。E2E 测试数量应保持最少,仅覆盖最重要的流程,以避免测试套件运行时间过长和维护成本激增。

Jest 的模拟(jest.fnjest.mock)允许测试在隔离环境中运行,通过替代不可控的外部依赖(如网络请求、数据库、文件系统)为可预测的模拟实现,使得测试快速、稳定、可重复。同时,模拟函数还可以通过 toHaveBeenCalled 等匹配器断言其被调用的方式和参数,验证组件或函数与依赖的交互是否正确。


课后练习答案

一、概念自测答案

  1. B

    • 解析:ittest 都用于定义测试用例;describe 用于组织测试套件;expect 用于断言;beforeEach 是生命周期钩子。
  2. B

    • 解析:React Testing Library 的设计哲学是“像用户一样测试”,不直接测试组件内部实现。
  3. jest.mock

    • 解析:jest.mock('module') 会将整个模块替换为模拟版本。
  4. B、C

    • 解析:Cypress 和 Playwright 是 E2E 测试工具。Jest 是单元/组件测试框架,React Testing Library 是组件测试工具。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请实现三个工具函数 capitalizerangegroupBy,并为它们编写完整的 Jest 测试套件。要求:

  • capitalize(str):处理字符串首字母大写,空字符串返回 '',非字符串输入抛出 TypeError
  • range(start, end, step=1):返回数字数组,处理 step 为正/负/0 的情况,以及不合法范围返回 []
  • groupBy(arr, key):对象数组按 key 分组,跳过缺少 key 的元素,返回 Record。空数组返回 {}
  • 测试文件使用 describe 分组,覆盖正常值、边界值、异常值,使用 expect 和相关匹配器。
  • 使用 CommonJS 导出和导入。输出 utils.jsutils.test.js 两个文件的完整内容。”
CATALOG
  1. 1. 第86课:前端测试基础——Jest、单元测试、组件测试、E2E 测试简介
    1. 1.1. 1. Jest 核心概念与快速开始
      1. 1.1.1. 1.1 安装与配置
      2. 1.1.2. 1.2 第一个测试用例
      3. 1.1.3. 1.3 分组与生命周期
    2. 1.2. 2. 单元测试:测试函数与工具方法
      1. 1.2.1. 2.1 测试纯函数
      2. 1.2.2. 2.2 测试异步代码
      3. 1.2.3. 2.3 模拟模块
    3. 1.3. 3. 组件测试:验证 UI 行为
      1. 1.3.1. 3.1 React 组件测试(React Testing Library)
      2. 1.3.2. 3.2 Vue 组件测试(Vue Test Utils + Testing Library)
    4. 1.4. 4. 快照测试(Snapshot Testing)
    5. 1.5. 5. 代码覆盖率
    6. 1.6. 6. E2E 测试概述:Cypress 与 Playwright
      1. 1.6.1. 6.1 Cypress
      2. 1.6.2. 6.2 Playwright
      3. 1.6.3. 6.3 测试金字塔
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、Agent 模式下的提示词示例
      4. 1.7.4. 四、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)