尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
06-高级模式与实战项目——10. 组件测试 - React Testing Library
10. 组件测试 - React Testing Library概述React Testing Library (RTL) 是一个用于测试 React 组件的工具库它鼓励编写以用户行为为中心的测试而不是测试实现细节。通过模拟用户交互验证组件的行为是否符合预期。维度内容What以用户行为为中心测试 React 组件Why测试组件的实际行为而非实现细节When测试组件渲染、用户交互、异步行为Where组件测试文件Who需要测试组件的开发者Howrender(Component /); fireEvent.click(screen.getByText(提交))1. RTL 基础1.1 安装配置npminstall--save-dev testing-library/react testing-library/jest-dom testing-library/user-event// setupTests.js import testing-library/jest-dom; // package.json { scripts: { test: react-scripts test, test:coverage: npm test -- --coverage } }1.2 基本测试// Button.jsx export default function Button({ children, onClick, disabled }) { return ( button onClick{onClick} disabled{disabled} {children} /button ); } // Button.test.jsx import { render, screen } from testing-library/react; import userEvent from testing-library/user-event; import Button from ./Button; describe(Button, () { test(渲染按钮文本, () { render(Button点击我/Button); expect(screen.getByText(点击我)).toBeInTheDocument(); }); test(点击时调用 onClick, async () { const handleClick jest.fn(); const user userEvent.setup(); render(Button onClick{handleClick}点击/Button); await user.click(screen.getByText(点击)); expect(handleClick).toHaveBeenCalledTimes(1); }); test(disabled 时不可点击, async () { const handleClick jest.fn(); const user userEvent.setup(); render(Button onClick{handleClick} disabled点击/Button); await user.click(screen.getByText(点击)); expect(handleClick).not.toHaveBeenCalled(); }); });2. 查询方法2.1 单元素查询import { render, screen } from testing-library/react; // getBy - 找不到时抛出错误 test(getBy 示例, () { render(divHello World/div); expect(screen.getByText(Hello World)).toBeInTheDocument(); // screen.getByText(Not Exist) // 抛出错误 }); // queryBy - 找不到时返回 null test(queryBy 示例, () { render(divHello World/div); expect(screen.queryByText(Not Exist)).toBeNull(); }); // findBy - 用于异步元素 test(findBy 示例, async () { render(AsyncComponent /); const element await screen.findByText(加载完成); expect(element).toBeInTheDocument(); });2.2 多元素查询function TodoList({ todos }) { return ( ul {todos.map(todo ( li key{todo.id}{todo.text}/li ))} /ul ); } test(查询多个元素, () { const todos [ { id: 1, text: 任务1 }, { id: 2, text: 任务2 }, { id: 3, text: 任务3 }, ]; render(TodoList todos{todos} /); // getAllBy - 返回数组 const items screen.getAllByRole(listitem); expect(items).toHaveLength(3); // queryAllBy - 返回数组或空数组 expect(screen.queryAllByText(不存在)).toHaveLength(0); // findAllBy - 异步返回数组 // const items await screen.findAllByRole(listitem); });2.3 查询优先级// 优先级从高到低 // 1. 可访问性查询推荐 screen.getByRole(button, { name: /提交/i }); screen.getByLabelText(用户名); screen.getByPlaceholderText(请输入邮箱); screen.getByText(确认); screen.getByDisplayValue(初始值); // 2. 语义查询 screen.getByAltText(用户头像); screen.getByTitle(关闭); // 3. 测试 ID最后选择 screen.getByTestId(submit-button);3. 用户交互3.1 使用 userEventimport { render, screen } from testing-library/react; import userEvent from testing-library/user-event; function LoginForm({ onSubmit }) { const [email, setEmail] useState(); const [password, setPassword] useState(); const handleSubmit (e) { e.preventDefault(); onSubmit({ email, password }); }; return ( form onSubmit{handleSubmit} input typeemail placeholder邮箱 value{email} onChange{(e) setEmail(e.target.value)} / input typepassword placeholder密码 value{password} onChange{(e) setPassword(e.target.value)} / button typesubmit登录/button /form ); } test(表单提交, async () { const handleSubmit jest.fn(); const user userEvent.setup(); render(LoginForm onSubmit{handleSubmit} /); await user.type(screen.getByPlaceholderText(邮箱), testexample.com); await user.type(screen.getByPlaceholderText(密码), password123); await user.click(screen.getByText(登录)); expect(handleSubmit).toHaveBeenCalledWith({ email: testexample.com, password: password123, }); });3.2 常用交互test(用户交互示例, async () { const user userEvent.setup(); // 点击 await user.click(screen.getByText(按钮)); await user.dblClick(screen.getByText(双击)); // 输入 await user.type(screen.getByRole(textbox), Hello); await user.clear(screen.getByRole(textbox)); // 选择 await user.selectOptions( screen.getByRole(combobox), option-value ); // 复选框 await user.click(screen.getByRole(checkbox)); // 键盘 await user.keyboard({Enter}); await user.tab(); // 悬停 await user.hover(screen.getByText(悬停)); await user.unhover(screen.getByText(悬停)); });4. 异步测试4.1 waitForfunction AsyncComponent() { const [data, setData] useState(null); useEffect(() { setTimeout(() setData(加载完成), 500); }, []); return div{data || 加载中...}/div; } import { render, screen, waitFor } from testing-library/react; test(等待异步内容, async () { render(AsyncComponent /); expect(screen.getByText(加载中...)).toBeInTheDocument(); await waitFor(() { expect(screen.getByText(加载完成)).toBeInTheDocument(); }); });4.2 findBytest(使用 findBy 等待异步内容, async () { render(AsyncComponent /); const element await screen.findByText(加载完成); expect(element).toBeInTheDocument(); });4.3 异步 API 测试// 需要 Mock API jest.mock(./api); function UserProfile({ userId }) { const [user, setUser] useState(null); const [loading, setLoading] useState(true); useEffect(() { fetchUser(userId).then(user { setUser(user); setLoading(false); }); }, [userId]); if (loading) return div加载中.../div; return div{user?.name}/div; } test(加载用户数据, async () { const mockUser { id: 1, name: 张三 }; fetchUser.mockResolvedValue(mockUser); render(UserProfile userId{1} /); expect(screen.getByText(加载中...)).toBeInTheDocument(); const userName await screen.findByText(张三); expect(userName).toBeInTheDocument(); });5. 完整示例Todo 应用测试// TodoApp.jsx import { useState } from react; export default function TodoApp() { const [todos, setTodos] useState([]); const [input, setInput] useState(); const addTodo () { if (input.trim()) { setTodos([...todos, { id: Date.now(), text: input, completed: false }]); setInput(); } }; const toggleTodo (id) { setTodos(todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo )); }; const deleteTodo (id) { setTodos(todos.filter(todo todo.id ! id)); }; return ( div h1待办事项/h1 div input value{input} onChange{(e) setInput(e.target.value)} placeholder添加新任务 / button onClick{addTodo}添加/button /div ul {todos.map(todo ( li key{todo.id} span style{{ textDecoration: todo.completed ? line-through : none }} onClick{() toggleTodo(todo.id)} {todo.text} /span button onClick{() deleteTodo(todo.id)}删除/button /li ))} /ul /div ); } // TodoApp.test.jsx import { render, screen } from testing-library/react; import userEvent from testing-library/user-event; import TodoApp from ./TodoApp; describe(TodoApp, () { test(初始状态没有待办事项, () { render(TodoApp /); expect(screen.queryByRole(listitem)).not.toBeInTheDocument(); }); test(可以添加待办事项, async () { const user userEvent.setup(); render(TodoApp /); const input screen.getByPlaceholderText(添加新任务); const addButton screen.getByText(添加); await user.type(input, 学习 React 测试); await user.click(addButton); expect(screen.getByText(学习 React 测试)).toBeInTheDocument(); }); test(不能添加空白任务, async () { const user userEvent.setup(); render(TodoApp /); const addButton screen.getByText(添加); await user.click(addButton); expect(screen.queryByRole(listitem)).not.toBeInTheDocument(); }); test(可以切换待办事项状态, async () { const user userEvent.setup(); render(TodoApp /); const input screen.getByPlaceholderText(添加新任务); const addButton screen.getByText(添加); await user.type(input, 完成这个任务); await user.click(addButton); const todoText screen.getByText(完成这个任务); expect(todoText).toHaveStyle({ textDecoration: none }); await user.click(todoText); expect(todoText).toHaveStyle({ textDecoration: line-through }); }); test(可以删除待办事项, async () { const user userEvent.setup(); render(TodoApp /); const input screen.getByPlaceholderText(添加新任务); const addButton screen.getByText(添加); await user.type(input, 待删除任务); await user.click(addButton); expect(screen.getByText(待删除任务)).toBeInTheDocument(); const deleteButton screen.getByText(删除); await user.click(deleteButton); expect(screen.queryByText(待删除任务)).not.toBeInTheDocument(); }); test(可以添加多个待办事项, async () { const user userEvent.setup(); render(TodoApp /); const input screen.getByPlaceholderText(添加新任务); const addButton screen.getByText(添加); await user.type(input, 任务1); await user.click(addButton); await user.type(input, 任务2); await user.click(addButton); await user.type(input, 任务3); await user.click(addButton); const items screen.getAllByRole(listitem); expect(items).toHaveLength(3); }); });6. 总结核心要点要点说明核心原则测试用户行为而非实现细节查询方法getBy、queryBy、findBy用户交互userEvent 模拟真实操作异步处理waitFor、findBy测试优先级可访问性查询getByRole、getByLabelText语义查询getByText、getByPlaceholderText测试 IDgetByTestId记忆口诀RTL 测试用户行不测实现细节好查询优先选 role交互用 userEvent异步等待 waitFor组件行为全测到7. 相关资源React Testing Library 文档Jest DOM 匹配器User Event 文档
RELATED

相关推荐

华为加速商业市场AI落地:要让鲲鹏昇腾算力像用水电一样便利

华为加速商业市场AI落地:要让鲲鹏昇腾算力像用水电一样便利

进入2026年以来,从“全民养虾潮”的现象级破圈,到AI深度嵌入企业生产经营,AI正加速从“生成内容”迈向“执行任务”的实质性落地阶段。在赋能千行百业的过程中,拥抱AI不再是大企业的专利,超6000万家中小企业正从观望者…

📅 2026/10/23 21:10:45
HarmonyKit | 鸿蒙新特性:cryptoFramework 异步加密哈希计算实战

HarmonyKit | 鸿蒙新特性:cryptoFramework 异步加密哈希计算实战

HarmonyKit | 鸿蒙新特性:cryptoFramework 异步加密哈希计算实战 引言:cryptoFramework 的异步哲学 HarmonyKit 的哈希计算工具是唯一使用 kit.CryptoArchitectureKit 的工具。鸿蒙加密框架提供了完整的密码学原语——哈希、对称/非对称加密、数字签名…

📅 2026/10/6 18:10:24
礼鼎半导体的IC载板生意,能撑起150亿估值吗?

礼鼎半导体的IC载板生意,能撑起150亿估值吗?

AI芯片火了,英伟达、台积电几乎家喻户晓。但很少有人知道,一颗AI芯片真正装进服务器之前,还离不开一块只有几厘米大小的"IC载板"。 礼鼎半导体科技(深圳)股份有限公司(简称礼鼎半导体&#xff09…

📅 2026/10/7 8:04:01
MORE NEWS

更多资讯

📰

附录 A · 工具速查表

附录导读本附录是混淆工具和反混淆工具的快速索引,按用途分类,相当于一本"工具字典":第 1 节:混淆工具——把代码变难分析的"加固方"武器;第 2 节:反混淆工具——把混淆代码还原回去的…

📰

PDO属性设置全解析:高效数据库操作秘诀

PDO::关于PHP的PDO这种技术, 它有一份参考手册。PDO:: — 设置属性(PHP 5 > 5.1.0, PECL pdo > 0.1.0)说明语法bool PDO::setAttribute ( int $attribute , mixed $value )我们需要把数据库句柄的属性给设置好。在下文之中, 会列举出一些可以被大家所用到的通用的属性内容…

📰

GMSK调制与基于Laurent分解的GMSK解调FPGA实现,第一篇(GMSK调制篇)

目录 一、GMSK简介 二、GMSK调制原理与步骤 1、GMSK调制原理 2、GMSK调制步骤 三、GMSK调制MATLAB仿真验证 1、GMSK调制MATLAB代码 2、GMSK调制MATLAB仿真 四、GMSK调制FPGA实现 1、GMSK调制FPGA Verilog代码 2、GMSK调制FPGA 仿真 五、FPGA和MATLAB联合仿真 1、…

📰

JAVA——抽象类、内部类和接口

一、抽象类如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类可以包含抽象方法与普通方法和属性。在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽…

📰

二叉树的学习

二叉树 1.树形概念 想了解二叉树的概念我们得先知道数的概念 树是⼀种⾮线性的数据结构,它是由n(n>0)个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下…

📰

DouK-Downloader 实操指南:从 Cookie 配置到 Docker 部署的抖音/TikTok 批量下载与数据采集

DouK-Downloader 实操指南:从 Cookie 配置到 Docker 部署的抖音/TikTok 批量下载与数据采集 【免费下载链接】TikTokDownloader 抖音 / TikTok 平台作品下载/数据采集工具 项目地址: https://gitcode.com/GitHub_Trending/ti/TikTokDownloader 动手前&#x…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬