> 技术文档 > 前端单元测试:Jest从入门到精通

前端单元测试:Jest从入门到精通


前言

在当今快节奏的前端开发中,单元测试毫无疑问是保障代码质量的重要防线。作为 Facebook 开源的测试框架,Jest 以其零配置、强大功能和开发者友好性,成为前端测试的首选工具。本文将带你掌握 Jest 单元测试的核心技能,一起写更优秀的代码吧!

一、Jest 核心特性

  1. 零配置:开箱即用,大部分项目无需配置

  2. 快照测试:轻松捕获组件渲染结构

  3. 异步支持:完善处理 Promise、async/await

  4. Mock 系统:强大的函数/模块模拟能力

  5. 代码覆盖率:内置覆盖率报告生成

二、安装Jest

npm install --save-dev jest# 或yarn add --dev jest

三、常见的 Jest 命令行操作

1、 只会跑测试未通过的用例,再次点击 f 会取消当前模式。

2、o  只监听已改变的文件,如果存在多个测试文件,可以开启,会与当前 git 仓库中的提交进行比较,需要使用 git 来监听哪个文件修改了,也可以将 --watchAll 改为 --watch 只会运行修改的文件。

3、a  运行所有测试,如果在 watch 模式中使用了 f 或 o ,使用 a 可以恢复运行所有测试。

4、u  用于更新 Jest 快照测试中的快照。如果更改了渲染组件的输出,可以使用此命令更新快照。

5、w  显示 Jest watch 模式中的所有可用命令和选项的列表。

6、q  退出 Jest 的 watch 模式。

7、 只会运行之前运行失败的测试文件,但提供更交互式的体验。

四、Jest核心功能

1、测试命名规范

describe(\'模块/组件名称\', () => { it(\'应该...当...\', () => { ... }); it(\'不应该...当...\', () => { ... });});

2、测试优化技巧

// 跳过慢测试describe.only(\'关键功能\', () => { ... });// 并行优化describe.concurrent(\'性能测试\', () => { ... });

3、常用匹配器(断言方法)

断言 用途 示例 toBe 严格相等 expect(1).toBe(1) toEqual 深度比较 expect({a:1}).toEqual({a:1}) toBeTruthy 真值检查 expect(\'text\').toBeTruthy() toContain 包含检查 expect([\'a\',\'b\']).toContain(\'a\') toMatchSnapshot 快照匹配 expect(component).toMatchSnapshot()

4、React Testing Library(提供用于测试 React 组件的函数)

渲染相关:

render :是一个核心函数,它的作用是将 React 组件渲染到一个虚拟的 DOM 环境中,以便我们能够测试组件的行为和输出

rerender重新渲染组件(更新 props)例:rerender( );}

// Button.test.jsximport React from \'react\';import { render, screen, fireEvent } from \'@testing-library/react\';import Button from \'./Button\';describe(\'Button Component\', () => { it(\'renders correctly with children\', () => { render(); expect(screen.getByTestId(\'action-button\')) .toHaveTextContent(\'Click Me\'); }); it(\'triggers onClick callback\', () => { const handleClick = jest.fn(); render(); fireEvent.click(screen.getByRole(\'button\')); expect(handleClick).toHaveBeenCalledTimes(1); });});

代码详解:

1)导入依赖

import React from \'react\';import { render, screen, fireEvent } from \'@testing-library/react\';import Button from \'./Button\';
  • 导入 React 和必要的测试工具

  • 从 @testing-library/react 导入 render(渲染组件)、screen(访问 DOM)和 fireEvent(模拟事件)

  • 导入要测试的 Button 组件

2)测试套件描述

describe(\'Button Component\', () => {

创建名为 \"Button Component\" 的测试套件,包含所有关于这个组件的测试用例。

3) 第一个测试用例:渲染测试

it(\'renders correctly with children\', () => { render(); expect(screen.getByTestId(\'action-button\')) .toHaveTextContent(\'Click Me\');});
  • 渲染 

  • 使用 screen.getByTestId(\'action-button\') 查找带有 data-testid=\"action-button\" 属性的元素

  • 断言该元素包含文本 \"Click Me\"

注意:这里假设 Button 组件内部有一个 data-testid=\"action-button\" 的属性

4)第二个测试用例:点击事件测试

it(\'triggers onClick callback\', () => { const handleClick = jest.fn(); render(); fireEvent.click(screen.getByRole(\'button\')); expect(handleClick).toHaveBeenCalledTimes(1);});
  • 创建一个模拟函数 handleClick = jest.fn()

  • 渲染带 onClick 处理函数的 

  • 使用 fireEvent.click() 模拟点击按钮(通过 getByRole 查找按钮元素)

  • 断言 handleClick 被调用了 1 次

2、高阶组件测试技巧

测试高阶主键的关键点:

  1. 验证渲染的组件是否正确

  2. 测试注入的 props 是否符合预期

  3. 检查 HOC 添加的功能是否正常工作

  4. 确保上下文和依赖正确处理

  5. 验证静态方法和显示名称是否正确保留

示例:测试高阶组件的基本渲染

import React from \'react\';import { render } from \'@testing-library/react\';import withEnhancement from \'./withEnhancement\';// 创建一个简单的测试组件const TestComponent = ({ value }) => 
{value}
;// 应用高阶组件const EnhancedComponent = withEnhancement(TestComponent);test(\'HOC 应该正确渲染包装的组件\', () => { const { getByText } = render(); expect(getByText(\'test\')).toBeInTheDocument();});

代码详解:

1)导入依赖

import React from \'react\';import { render } from \'@testing-library/react\';import withEnhancement from \'./withEnhancement\';
  • React:必须导入,因为我们要使用 JSX 语法

  • render:从 @testing-library/react 导入,用于渲染组件进行测试

  • withEnhancement:这是我们要测试的高阶组件,从本地文件导入

2) 创建测试组件

const TestComponent = ({ value }) => 
{value}
;
  • 这是一个简单的\"哑组件\"(dumb component),只接收一个 value prop 并渲染它

  • 我们使用这个简单组件来测试高阶组件的行为,因为它没有自己的复杂逻辑

  • 这样的测试组件有助于隔离 HOC 的行为,避免被组件自身逻辑干扰

3)应用高阶组件

const EnhancedComponent = withEnhancement(TestComponent);
  • 这里我们调用 withEnhancement 高阶组件函数,传入我们的 TestComponent

  • 结果是创建了一个新的增强组件 EnhancedComponent

  • 这个新组件应该包含 withEnhancement 提供的所有增强功能

4)测试用例

test(\'HOC 应该正确渲染包装的组件\', () => { const { getByText } = render(); expect(getByText(\'test\')).toBeInTheDocument();});

测试描述:

\'HOC 应该正确渲染包装的组件\' - 这个描述清楚地说明了我们正在测试高阶组件是否正确地渲染了它包装的组件

渲染组件:

render():

  • 使用 @testing-library/react 的 render 方法渲染增强后的组件

  • 我们传递 value=\"test\" 作为 prop,这将传递给原始的 TestComponent

获取查询方法:

const { getByText } = render(...):

  • render 方法返回一个对象,包含多种查询方法

  • 我们解构出 getByText,它允许我们通过文本内容查找元素

断言:

expect(getByText(\'test\')).toBeInTheDocument():

  • getByText(\'test\') 查找包含文本 \"test\" 的元素

  • toBeInTheDocument() 断言该元素确实存在于渲染的 DOM 中

  • 如果这个断言通过,说明:

    1. 高阶组件正确地渲染了包裹的 TestComponent

    2. value prop 被正确地传递给了 TestComponent

    3. 整个组件层次结构按预期工作

八、异步代码测试

1、api请求测试

// api.jsexport async function fetchUser(id) { const response = await fetch(`/users/${id}`); if (!response.ok) throw new Error(\'Network error\'); return response.json();}
// api.test.jsimport { fetchUser } from \'./api\';import { setupServer } from \'msw/node\';import { rest } from \'msw\';const server = setupServer( rest.get(\'/users/1\', (req, res, ctx) => { return res(ctx.json({ id: 1, name: \'Alice\' })); }));beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());describe(\'fetchUser\', () => { it(\'returns user data\', async () => { const user = await fetchUser(1); expect(user).toEqual({ id: 1, name: \'Alice\' }); }); it(\'handles network errors\', async () => { server.use( rest.get(\'/users/1\', (req, res, ctx) => { return res(ctx.status(500)); }) ); await expect(fetchUser(1)).rejects.toThrow(\'Network error\'); });});

Mock Service Worker (MSW) : 是一个用于拦截和模拟 HTTP 请求的库,非常适合在测试中模拟 API 行为

代码详解

1)核心依赖

import { fetchUser } from \'./api\'; // 要测试的 API 函数import { setupServer } from \'msw/node\'; // Node 环境下的 MSW 服务import { rest } from \'msw\'; // REST API 请求拦截工具

2) 设置 Mock Server

const server = setupServer( rest.get(\'/users/1\', (req, res, ctx) => { return res(ctx.json({ id: 1, name: \'Alice\' })); }));
  • setupServer: 创建一个模拟的 API 服务。

  • rest.get: 拦截 GET /users/1 请求,并返回模拟响应 { id: 1, name: \'Alice\' }

  • ctx.json(): 构造 JSON 格式的响应体。


3) 生命周期管理

beforeAll(() => server.listen()); // 启动 mock serverafterEach(() => server.resetHandlers()); // 重置 mock(避免测试间污染)afterAll(() => server.close()); // 关闭 mock server
  • server.listen(): 启动拦截。

  • server.resetHandlers(): 每个测试后清理 mock(避免跨测试影响)。

  • server.close(): 所有测试完成后关闭。


4) 测试正常请求

it(\'returns user data\', async () => { const user = await fetchUser(1); expect(user).toEqual({ id: 1, name: \'Alice\' });});
  • 调用 fetchUser(1),它会发送 GET /users/1 请求。

  • 被 MSW 拦截后返回模拟数据,断言结果是否符合预期。


5) 测试异常请求

it(\'handles network errors\', async () => { server.use( rest.get(\'/users/1\', (req, res, ctx) => { return res(ctx.status(500)); // 模拟服务器错误 }) ); await expect(fetchUser(1)).rejects.toThrow(\'Network error\');});
  • server.use: 动态覆盖之前的 mock,返回 500 错误。

  • rejects.toThrow: 验证 fetchUser 是否正确处理了错误。

扩展场景:

模拟延迟

rest.get(\'/users/1\', (req, res, ctx) => { return res(ctx.delay(100), ctx.json({ id: 1 })); // 延迟 100ms});

动态路径参数

rest.get(\'/users/:id\', (req, res, ctx) => { const { id } = req.params; return res(ctx.json({ id }));});

九、测试覆盖率

1、生成覆盖率报告

npx jest --coverage

输出示例:

----------------|---------|----------|---------|---------|-------------------File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s----------------|---------|----------|---------|---------|-------------------src/ | 80 | 75 | 90 | 85 | utils.js | 100 | 100 | 100 | 100 | api.js | 50 | 50 | 66 | 60 | 15-20, 25----------------|---------|----------|---------|---------|-------------------
  • % Stmts(语句覆盖率):代码中有多少语句被执行过。

  • % Branch(分支覆盖率)if/elseswitch 等分支是否都被覆盖。

  • % Funcs(函数覆盖率):有多少函数被调用过。

  • % Lines(行覆盖率):有多少行代码被执行过。

  • Uncovered Line #s:未被覆盖的行号。

html报告(更详细,位于 coverage/lcov-report/index.html):

  • 不同颜色标注覆盖/未覆盖代码:

    • 绿色:已覆盖

    • 红色:未覆盖

    • 黄色:部分覆盖(如分支未完全覆盖)

操作 命令/配置 生成报告 jest --coverage 自定义阈值 coverageThreshold 排除文件 collectCoverageFrom 查看详情 打开 coverage/lcov-report/index.html 提高覆盖率 补全分支测试、错误测试、边界测试

记住:好的测试不是追求 100% 覆盖率,而是在关键路径上建立可靠的防护网。开始为你的项目编写测试吧,代码质量提升的效果会让你惊喜!