为React组件库引入自动化测试:从零到完善的实践之路
为什么我们需要测试?
我们的 React+TypeScript 业务组件库已经稳定运行了一段时间,主要承载各类UI展示组件,如卡片、通知等。项目初期,迫于紧张的开发周期,我们暂时搁置了自动化测试的引入。当时团队成员对组件逻辑了如指掌,即便没有测试也能游刃有余。
然而随着时间推移,问题逐渐显现。当新成员加入或老组件需要迭代时,我们常常陷入两难:修改代码可能破坏原有功能,但不修改又无法满足新需求。特别是在处理那些具有多种交互状态的复杂组件时,手动测试变得既耗时又不可靠。这时,引入自动化测试的必要性就凸显出来了。
搭建测试环境
依赖安装
我们首先从安装核心测试依赖开始,这些工具将构成我们测试体系的基础框架:
- 测试运行核心:jest和jsdom环境包
- TypeScript支持:确保类型安全的测试环境
- React测试工具:专门为React组件设计的测试工具链
npm install jest jest-environment-jsdom @types/jest ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event --save-dev
配置Jest
创建jest.config.ts配置文件时,有几个关注点:
- 针对TypeScript项目的特殊处理
- 浏览器环境的模拟
- 测试初始化流程
- 文件转换规则
module.exports = {
preset: "ts-jest", // 为 TypeScript 项目准备的 Jest 配置预设
testEnvironment: "jsdom", // 测试运行在模拟的浏览器环境中
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], // 指定在测试环境初始化后立即执行的文件
transform: {
"^.+\\.(ts|tsx)$": "ts-jest", // 使用 ts-jest 处理所有 .ts 和 .tsx 文件
},
testPathIgnorePatterns: ["/node_modules/", "/dist/"], // 忽略指定目录下的测试文件
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], // 定义 Jest 能识别的模块文件扩展名
};
export {}; // 使文件成为模块
创建jest.setup.ts文件引入断言库:
import "@testing-library/jest-dom";
TypeScript配置
修改tsconfig.json包含测试相关文件:
{
"include": [
"src",
"jest.config.ts",
"jest.setup.ts",
"__mocks__/**/*.ts"
]
}
测试用例编写
我们以一个通知组件为例,该组件有两种UI形态:
- 标题和描述组合的时间内容文案提示
- 带有喇叭图标的提示,点击关闭按钮时调用接口保存用户状态
特殊依赖处理
组件中有三类特殊引入需要处理:
import './index.less';
import { noticeIcon, closeIcon } from "$src/common/icon";
import request from "$src/request";
1、处理 CSS/LESS 资源
Jest 默认无法解析 CSS/LESS 文件,我们可以通过配置将其模拟为空对象:
// jest.config.js
module.exports = {
moduleNameMapper: {
"\\.(less|css)$': '<rootDir>/__mocks__/styleMock.ts", // 指向一个空文件
},
};
// __mocks__/styleMock.ts
module.exports = {};
2、配置路径别名
对于 $src
这样的路径别名,需要在 Jest 配置中映射:
// jest.config.js
module.exports = {
moduleNameMapper: {
'^\\$src/(.*)$': '<rootDir>/src/$1',
},
};
3、模拟图标资源
对于图标这类静态资源,我们可以在测试文件中直接模拟:
// __tests__/index.test.tsx
jest.mock('$src/common/icon', () => ({
noticeIcon: 'notice-icon-path',
closeIcon: 'close-icon-path',
}));
4、模拟 API 请求
对于网络请求模块,我们可以将其转换为 Jest 模拟函数:
// __tests__/index.test.tsx
import request from '$src/request';
const mockedRequest = request as jest.MockedFunction<typeof request>;
jest.mock('$src/request', () => ({
__esModule: true, // 标识这是 ES Module
default: jest.fn(() => Promise.resolve({ data: {} })),
}));
通过以上配置,我们能够有效地隔离组件测试环境,专注于组件逻辑本身的测试,而不受样式、静态资源和网络请求等外部因素的影响。
基础测试框架搭建
我们首先建立测试的基本结构:
describe("Notification组件", () => {
// 公共props定义
const baseProps = {
body: {},
tokenId: "test-token",
urlPrefix: "https://api.example.com",
};
// 每个测试用例前的清理工作
beforeEach(() => {
jest.clearAllMocks();
mockedRequest.mockReset();
});
});
核心测试场景覆盖
在配置好 Jest 测试环境后,我们将针对通知组件编写全面的测试用例。该组件具有两种展示形态和交互逻辑,我们将从四个关键维度进行测试覆盖:
1、边界情况测试
我们首先考虑最极端的场景——当传入无效props时,组件是否能够优雅处理:
it("当传入无效body时应安全地返回null", () => {
const { container } = render(<Notification {...baseProps} body={null} />);
expect(container.firstChild).toBeNull();
});
2、日期类型展示验证
对于日期类型的通知,我们需要确认:
- 关键文本是否正确渲染
- DOM结构是否符合预期
- 样式类是否准确应用
it("应正确渲染日期类型通知", () => {
render(<Notification {...dateProps} />);
expect(screen.getByText("今日公告")).toBeInTheDocument();
expect(screen.getByText("2023-06-15")).toBeInTheDocument();
const dateContainer = screen.getByText("今日公告").parentElement;
expect(dateContainer).toHaveClass("notice-header-date");
});
3、广播类型交互测试
广播通知的测试更加复杂,需要验证:
- 初始状态下的元素展示
- 图标资源是否正确加载
- 点击关闭后的行为
it("点击关闭按钮后应隐藏通知内容", async () => {
render(<Notification {...broadcastProps} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
fireEvent.click(closeButton!);
await waitFor(() => {
expect(screen.queryByText("重要通知内容")).not.toBeInTheDocument();
});
});
4、网络请求场景全覆盖
对于涉及API调用的场景,我们设计了多维度测试:
- 正常请求流程
- 无请求场景请求
- 失败处理
- 请求中的状态管理
describe("网络请求测试", () => {
const broadcastPropsWithCloseUrl = {
...baseProps,
body: {
type: BROADCAST_TYPE,
content: "重要通知内容",
closeUrl: "/close-notice",
},
};
const broadcastPropsWithoutCloseUrl = {
...baseProps,
body: {
type: BROADCAST_TYPE,
content: "重要通知内容",
// 没有closeUrl
},
};
it("点击关闭时应该发送请求", async () => {
// 模拟请求成功
mockedRequest.mockResolvedValue({ data: {} });
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
//验证请求参数
expect(request).toHaveBeenCalledWith({
url: "https://api.example.com/close-notice",
method: "post",
data: {},
headers: {
tokenId: "test-token",
},
});
// 验证UI更新
expect(screen.queryByText("重要通知内容")).not.toBeInTheDocument();
});
it("当没有closeUrl时不发送请求", async () => {
render(<Notification {...broadcastPropsWithoutCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
expect(request).not.toHaveBeenCalled();
// 验证UI仍然会更新
expect(screen.queryByText("重要通知内容")).not.toBeInTheDocument();
});
it("请求失败时仍然关闭通知", async () => {
// 模拟请求失败
mockedRequest.mockResolvedValue(new Error("Request failed"));
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
await act(async () => {
fireEvent.click(closeButton!);
});
// 验证即使请求失败,UI也会更新
expect(screen.queryByText("重要通知内容")).not.toBeInTheDocument();
expect(request).toHaveBeenCalled();
});
it("请求期间UI应保持响应", async () => {
// 创建一个未立即resolve的Promise
let resolveRequest: any;
const promise = new Promise((resolve) => {
resolveRequest = resolve;
});
mockedRequest.mockReturnValue(promise);
render(<Notification {...broadcastPropsWithCloseUrl} />);
const closeButton = screen.getAllByRole("img")[1].parentElement;
// 第一次点击
fireEvent.click(closeButton!);
// 验证UI已立即更新
expect(screen.queryByText("重要通知内容")).not.toBeInTheDocument();
// 完成请求
await act(async () => {
resolveRequest({ data: {} });
});
});
测试执行与覆盖率
基础测试执行
在完成通知组件的测试用例编写后,可以在 package.json 中配置测试脚本:
{
"scripts": {
"test": "jest"
}
}
执行 npm run test 命令后,如下图所示,Jest 会在终端输出测试结果,包括:
- 测试文件数量
- 通过的测试用例数
- 失败的测试用例详情(包含错误堆栈信息)
覆盖率报告配置
为了更全面地评估测试质量,可以通过修改 jest.config.ts 启用覆盖率统计:
module.exports = {
collectCoverage: true, // 启用覆盖率收集
coverageDirectory: "coverage", // 指定覆盖率报告的输出目录
coverageReporters: ["text", "html", "lcov", "clover"], //指定生成的覆盖率报告格式
coverageThreshold: {
// 设置覆盖率的最低阈值,如果未达标,Jest 会报错
global: {
// 全局覆盖率要求
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
"./src/components/**/*.tsx": {
// 针对特定目录/文件设置更高要求
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
}
执行测试后:终端会显示各维度的覆盖率百分比,在 coverage/ 目录下生成详细报告:index.html 提供可视化分析可逐层查看未覆盖的代码路径。
示例输出中显示 common/util.ts 仅 32.39% 覆盖率,低于预设阈值。此时应该优先补充核心工具函数的测试用例。通过持续完善测试覆盖,可以有效提升组件迭代的可靠性,并为后续重构提供安全保障。
通过引入自动化测试,我们实现了从"人肉测试"到系统化保障的转变。精心设计的测试用例覆盖了各种边界情况,配合覆盖率分析,构建了多层次的质量防护体系。
如果你对前端工程化有兴趣,或者想了解更多相关的内容,欢迎查看我的其他文章,这些内容将持续更新,希望能给你带来更多的灵感和技术分享~
为React组件库引入自动化测试:从零到完善的实践之路的更多相关文章
- 如何快速构建React组件库
前言 俗话说:"麻雀虽小,五脏俱全",搭建一个组件库,知之非难,行之不易,涉及到的技术方方面面,犹如海面风平浪静,实则暗礁险滩,处处惊险- 目前团队内已经有较为成熟的 Vue 技术 ...
- 七个不可错过的React组件库与开发框架
React是如今最火爆的前端技术,而React最棒的一个特点就是有大量功能丰富的组件库和开发框架可用.从按钮到卷轴到工具条,应有尽有,而且这些组件可以各行其是,也可以组装成复杂的UI,你也可以把UI分 ...
- React组件库
图表组件库:Recharts(React和D3构建的图表库) UI组件库:react-bootstrap
- React 组件库框架搭建
前言 公司业务积累了一定程度,需要搭建自己的组件库,有了组件库,整个团队开发效率会提高恨多. 做组件库需要提供开发调试环境,和组件文档的展示,调研了几个比较主流的方案,如下: docz 配置简单,功能 ...
- React组件库集锦及学习视频
[转载]https://www.rails365.net/articles/react-zui-hao-de-ui-zu-jian-ku-ji-jin 这里有一篇讨论,说了哪个才是 React 最好的 ...
- [翻译]怎么写一个React组件库(二)
本文同步发布于知乎专栏 https://zhuanlan.zhihu.com/p/27434018,喜欢本文的就去知乎点个赞支持下吧- 引言 该系列文章将通过创建一个组件库来引导你学习如何构建自己的组 ...
- [翻译]怎么写一个React组件库(一)
本文同步发布于知乎专栏 https://zhuanlan.zhihu.com/p/27401329,喜欢本文的就去知乎点个赞支持下吧- 引言 该系列文章将通过创建一个组件库来引导你学习如何构建自己的组 ...
- 前端如何搭建vue UI组件库/封装插件(从零到有)
需求 因之前是做外包项目居多,经常用到相同的组件,之前的办法是在一个项目中写一个组件,其他项目直接将compents下的组件复制,粘贴到项目中使用,缺点是维护起来,改一个项目,其他项目也需要修改,所以 ...
- React组件库Ant Design的安装与使用
一.什么是 Ant Design 1.Ant Design 提炼自企业级中后台产品的交互语言和视觉风格 2.Ant Design 使用 TypeScript 构建,提供完整的类型定义文件 二.Ant ...
- 使用dumi生成react组件库文档并发布到github pages
周末两天玩了下号称西湖区东半球最牛逼的react文档站点生成工具dumi,顺带结合github pages生成了react-uni-comps文档站, 一套弄下来,感觉真香,现在还只是浅尝,高级的特性 ...
随机推荐
- 记录一次WPF程序进程挂起问题
## 1. 使用背景 开发`WPF`单进程项目,在项目中使用`MongoDB`数据库,需要连接多个不同的数据库实例,另外项目框架采用了事件聚合器来管理模块间的通知调用,基于`NetMQ`实现了一个`Z ...
- SWD下载口的端口状态
1.关于SWD SWD下载口的端口状态:SWD为上拉,SWC为下拉. SWD是MCU下载程序和调试的端口,分为四线制和五线制 四线制:VCC GND SWDIO SWCKL 五线制:VCC GND S ...
- (自适应手机端)合同模板网站源码 合同范文类网站pbootcms模板
PbootCMS内核开发的网站模板,该模板适用于合同范文网站.合同模板网站等企业,当然其他行业也可以做,只需要把文字图片换成其他行业的即可: pc+wap,同一个后台,数据即时同步,简单适用!附带测试 ...
- SSM - 狂神的项目示例
出于对狂神的崇拜,总结SSM项目. 基本介绍 项目分层 基本介绍 项目名称:ssmbuild 介绍:通过书籍管理系统实现一个简单的SSM项目,可以作为其他Java Web项目的借鉴. 主要功能模块:查 ...
- C# 一般处理程序ashx用于验证码
1.用VS2019建立一个web应用程序,选mvc模板 2.选中项目邮件新建文件夹Webservice,然后添加一般处理程序Verify.ashx然后右键打开改写如下 1 public class V ...
- 大模型知识引擎 LKE 新手入门指南:官方文档难懂?看这篇就够了
昨天简单体验了一下大模型知识引擎(LKE),总体来说,虽然其功能方面还有一定欠缺,但在一些特定领域,特别是RAG(Retrieval-Augmented Generation)技术的应用上,还是表现出 ...
- Error: EACCES: permission denied
前言 在安装 vue 脚手架的时候,报错 Error: EACCES: permission denied # 安装 Vue CLI 3.x npm i -g @vue/cli 解决 执行以下命令就可 ...
- Docker | 如何在 Linux 服务器使用 docker
目录 00 需求 01 安装 docker 02 准备工作 03 配置 Dockerfile 和 docker composer 04 启动 docker 05 测试能否 ssh 连接这个 docke ...
- JOKER 前端框架:自带云端可视化 IDE 的创新利器
一.框架概述 在当今前端开发的多元化领域中,JOKER 前端框架犹如一颗璀璨的明星,以其独特而强大的功能,为开发者开辟了一条高效.便捷的创新之路. JOKER 不仅仅是一个普通的框架,它更是一个综合性 ...
- [每日算法 - 华为机试] leetcode680. 验证回文串 II
入口 力扣https://leetcode.cn/problems/valid-palindrome-ii/submissions/ 题目描述 给你一个字符串 s,最多 可以从中删除一个字符. 请你判 ...