
React 单元测试完全指南从组件到 Hooks、Context 与可访问性测试【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine导读本文以 Refine 仓库中 documentation/blog/2024-08-05-react-unit-testing.md 为基础系统讲解 React 单元测试的完整知识体系从测试环境搭建、组件渲染与事件触发、状态与 Props 断言到 Mock 函数、自定义 Hooks、异步操作、Context、Router、Snapshot、性能与可访问性测试。文章同时结合 Refine 仓库真实的 Jest 配置documentation/jest.config.js与核心包 hooks 的测试用例帮助你不仅学会怎么写测试更理解测试工具链在真实开源项目中如何落地。读完本文你将能够为 React 组件、Hooks 与 Context 编写可维护、可复用的单元测试并掌握render、renderHook、fireEvent、waitFor、act、toMatchSnapshot、jest-axe等核心 API。为什么单元测试对 React 应用至关重要单元测试Unit Testing是软件开发流程中的关键环节开发者有时也包括 QA对软件中最小的功能单元逐一进行验证确保其行为符合预期。单元测试的价值在于建立信心让我们确信软件在绝大多数使用场景下都能正常工作即便是在最刁钻的边界用例中。提前暴露缺陷以add函数为例它接受两个数字参数。现实中用户可能输入单词、字符甚至符号若没有测试这类输入会导致应用崩溃。通过单元测试我们在部署前就能发现并重构add函数来正确处理这些场景。保护市场口碑用户一旦发现 bug 且修复不及时可能直接放弃该产品。测试覆盖是公司级软件的硬性要求开发者必须为每个功能编写测试否则软件会被视为风险。近年来涌现了大量测试工具让 React 框架下的单元测试变得非常简单Java 生态有 JUnitPHP 有 PHPUnitJavaScript 生态则有 Jest、Mocha、Jasmine 等。本文将围绕Jest与React Testing Library展开。搭建测试环境创建项目并安装工具create-react-app test-prj cd test-prj本指南使用的两个核心工具JestFacebook 出品的流行测试框架API 简单开箱即用。React Testing Library构建在 DOM Testing Library 之上的测试工具专门用于测试 React 组件。使用create-react-app脚手架创建的项目会自带它若未安装可执行npm install --save-dev testing-library/react安装 Jestnpm install --save-dev jest测试文件放哪里测试代码写在.js/.ts文件中但文件名需要以.test为前缀例如add.test.js这样 Jest 与 React Testing Library 才能识别并收集它们。要在package.json的scripts中添加test脚本{ scripts: { test: jest } }之后运行npm run test或yarn run testJest 会搜索并收集所有*.test.js|ts|tsx文件并在测试环境中逐一执行。也可以将测试文件统一放入名为__tests__的目录Jest 同样会自动查找并运行其中的测试。仓库中的真实配置以 Refine 文档站为例Refine 仓库自身的文档站就采用 Jest TypeScript 的测试栈其配置位于 documentation/jest.config.jsconst { pathsToModuleNameMapper } require(ts-jest); const { compilerOptions } require(./tsconfig.json); const paths compilerOptions.paths ? compilerOptions.paths : {}; module.exports { preset: ts-jest, rootDir: ./, setupFilesAfterEnv: [rootDir/test/jest.setup.ts], testPathIgnorePatterns: [rootDir/node_modules/, rootDir/dist/], moduleNameMapper: { ...pathsToModuleNameMapper(paths, { prefix: rootDir/ }), \\.css$: identity-obj-proxy, }, displayName: documentation, transform: { ^.\\.svg$: rootDir/test/svgTransform.ts, ^.\\.(ts|tsx)?$: [ts-jest, { tsconfig: rootDir/tsconfig.test.json, diagnostics: { ignoreCodes: [2578] } }], }, moduleFileExtensions: [ts, tsx, js, jsx, json, node], testEnvironment: jsdom, };这份配置可以视为一个生产级的参考模板几个关键点preset: ts-jest让 Jest 直接编译 TypeScript 测试文件testEnvironment: jsdom提供浏览器 DOM 环境配合 React Testing Library 的render使用原文档中render在 DOM 中渲染组件与浏览器 DOM 类似正是依赖这个环境setupFilesAfterEnd此处为setupFilesAfterEnv加载 documentation/test/jest.setup.ts其中导入了testing-library/jest-dom为断言提供toBeInTheDocument()、toHaveFocus()等扩展匹配器也导入了testing-library/reactmoduleNameMapper将tsconfig.json中的路径别名映射到实际目录并把 CSS 导入替换为identity-obj-proxy避免样式文件干扰测试。对应依赖在 documentation/package.json 中可见jest^29.3.1、ts-jest^29.1.2、jest-environment-jsdom^29.3.1、testing-library/react^12.1.4、testing-library/react-hooks^8.0.0、testing-library/user-event^14.1.1、testing-library/jest-dom^5.16.4。仓库根目录的package.json则通过lerna run test --stream以 monorepo 方式批量执行所有子包的测试。为 React 组件编写单元测试基础渲染测试Hello World先写一个渲染 Hello World 的组件并验证它确实渲染出了这段经典问候import React from react; const HelloWorld () { return divHello World/div; }; export default HelloWorld;对应的测试文件HelloWorld.test.jsimport { render } from testing-library/react; import HelloWorld from ./HelloWorld; test(renders Hello World text, () { const { getByText } render(HelloWorld /); const helloWorldElement getByText(Hello World); expect(helloWorldElement).toBeInTheDocument(); });拆解这三个核心 APIrender将组件渲染进一个类似浏览器 DOM 的环境中即 Jest 的 jsdom 环境之后就可以用 DOM 相关的查询函数来测试组件getByText在渲染出的 DOM 中搜索传入的字符串返回对应的元素toBeInTheDocument断言调用它的 DOM 对象确实存在于文档中这是testing-library/jest-dom提供的匹配器正是 Refine 文档站在jest.setup.ts中全局导入它的原因。用>import React from react; const HelloWorld () { return div>import React from react; import { render } from testing-library/react; import HelloWorld from ./HelloWorld; test(renders Hello World text using getByTestId, () { const { getByTestId } render(HelloWorld /); const helloWorldElement getByTestId(hello-world); expect(helloWorldElement).toBeInTheDocument(); expect(helloWorldElement.textContent).toBe(Hello World); });这里用getByTestId(hello-world)查询data-testid为hello-world的元素断言其存在于文档中并通过toBe匹配器验证textContent等于Hello World。需要注意的是RTL 官方建议优先使用用户可感知的查询方式如getByText、getByRoledata-testid更适用于没有稳定文本/语义可用的场景例如动态列表项、canvas 容器。测试事件触发假设有一个计数器应用点击按钮时更新 DOM 展示的值import React, { useState } from react; const Counter ({ count }) { const [increment, setIncrement] useState(0); const handleIncrement () { setIncrement(increment 1); }; return ( div pIncrement: {increment}/p button onClick{handleIncrement}Increment/button /div ); }; export default Counter;increment状态展示在 DOM 中点击Increment按钮会让它加 1。要测试这一行为需要模拟一次真实点击import React from react; import { render, fireEvent } from testing-library/react; import Counter from ./Counter; test(increments count on button click, () { const { getByText } render(Counter /); const incrementElement getByText(Increment: 0); const buttonElement getByText(Increment); fireEvent.click(buttonElement); expect(incrementElement.textContent).toBe(Increment: 1); });React Testing Library 为 DOM 元素实例提供了click()方法更常见的写法是fireEvent.click(element)模拟用户实际点击。流程是先用getByText(Increment)拿到按钮的 DOM 实例触发click事件让increment状态加 1最后断言p元素的文本更新为Increment: 1。补充fireEvent适合简单事件模拟在需要更贴近真实用户交互如输入、聚焦、Tab 切换的场景官方更推荐testing-library/user-event。Refine 文档站的依赖中也包含了testing-library/user-event^14.1.1可配合setup后以user.click(...)的方式使用。测试组件的状态与 Props验证状态更新状态测试的核心是当某个动作执行后状态的当前值被正确更新。以带data-testid的计数器为例import React, { useState } from react; const Counter ({ count }) { const [count, setCount] useState(0); const increment () { setCount(count 1); }; return ( div p Count: span>import React from react; import { render, fireEvent } from testing-library/react; import Counter from ./Counter; test(increments count on button click, () { const { getByTestId } render(Counter /); const countElement getByTestId(count); const buttonElement getByTestId(button); fireEvent.click(buttonElement); expect(countElement.textContent).toBe(1); });点击按钮后count变为 1span[data-testidcount]的文本相应更新为1。验证 Props 渲染Props 是传入组件的属性。下面的组件通过count属性接收外部传入的值import React from react; const Counter ({ count }) { return ( div p Count: span>import React from react; import { render } from testing-library/react; import Counter from ./Counter; test(renders the count prop, () { const { getByTestId } render(Counter count{9} /); const countElement getByTestId(count); expect(countElement.textContent).toBe(9); });Mock 函数调用测试过程中我们往往不希望真实执行某些函数例如该函数有调用次数限制、涉及网络请求或第三方副作用。此时需要mock即创建该真实函数的哑版本。Jest 通过fn()API 创建并返回一个 mock 函数const mockFn jest.fn();mock 函数可以验证被调用的次数、返回值以及每次调用时传入的参数数组Jest 都会记录下来。示例一个组件通过done属性接收回调函数点击Call DONE按钮时调用它const Test ({ done }) { return ( div button onClick{done}Call DONE/button /div ); };测试中把 mock 函数传给done点击按钮后断言调用次数import React from react; import { render } from testing-library/react; import Test from ./Test; test(test mock function props is called, () { const fn jest.fn(); const { getByText } render(Test done{fn} /); const button getByText(Call DONE); button.click(); expect(fn).toHaveBeenCalledTimes(1); button.click(); expect(fn).toHaveBeenCalledTimes(2); });要点说明jest.fn()创建 mock 函数并传给组件的done属性获取按钮句柄后触发click事件mock 函数被调用一次官方推荐的断言方式是用toHaveBeenCalledTimes(n)等 jest-dom/jest 匹配器比直接断言fn.mock.calls更语义化、更健壮如需断言调用参数可用toHaveBeenCalledWith(...)或通过fn.mock.calls[0]访问第一次调用的参数数组。仓库实例Mock 在真实 hooks 测试中的应用Refine 核心包中认证相关 hooks 的测试大量使用 mock 与 spy。以 packages/core/src/hooks/auth/useForgotPassword/index.spec.ts 为例import { renderHook, waitFor } from testing-library/react; import { vi } from vitest; import { TestWrapper, act, queryClient } from test; import { useForgotPassword } from .; describe(useForgotPassword Hook, () { const mockAuthProvider { login: () Promise.resolve({ success: true }), check: () Promise.resolve({ authenticated: true }), onError: () Promise.resolve({}), logout: () Promise.resolve({ success: true }), }; beforeEach(() { vi.spyOn(console, error).mockImplementation((message) { if (message?.message Missing email) return; if (typeof message undefined) return; console.warn(message); }); }); it(succeed forgot password, async () { const { result } renderHook(() useForgotPassword(), { wrapper: TestWrapper({ authProvider: { ...mockAuthProvider, forgotPassword: (params) { if (!params?.[email]) return Promise.resolve({ success: false }); return Promise.resolve({ success: true }); }, }, }), }); const { mutate: forgotPassword } result.current; await act(async () { forgotPassword({ email: testtest.com }); }); await waitFor(() { expect(result.current.data?.success).toBeTruthy(); }); }); // ... });这段测试展示了企业级 hooks 测试的三板斧用TestWrapper注入带 mock provider 的上下文、用renderHook渲染 hook、用actwaitFor等待异步 mutation 完成后再断言。你可以从中看到mock 依赖、隔离真实副作用这一思想的直接落地。测试 React HooksrenderHook无需组件即可测试自定义 Hook测试自定义 Hook 时我们要验证使用该 Hook 的组件的行为与状态变化。以useCounter为例它管理计数器并提供加减函数import { useState } from react; const useCounter () { const [count, setCount] useState(0); const increment () { setCount((prevCount) prevCount 1); }; const decrement () { setCount((prevCount) prevCount - 1); }; return { count, increment, decrement }; }; export default useCounter;测试用例import { renderHook, act } from testing-library/react-hooks; import useCounter from ./useCounter; test(should increment and decrement counter correctly, () { const { result } renderHook(() useCounter()); const { count, increment, decrement } result.current; expect(count).toBe(0); act(() { increment(); }); expect(count).toBe(1); act(() { decrement(); }); expect(count).toBe(0); });关键概念renderHook来自testing-library/react-hooks包直接渲染 Hook无需把它塞进某个组件result.currentresult对象包含current属性其值正是useCounter的返回值从中可解构出count、increment、decrementact用于包裹异步或触发状态更新的代码确保所有状态更新都被正确处理后再做断言。仓库实例renderHook waitFor 测试异步 hooksRefine 核心包的权限控制 hookuseCan的测试packages/core/src/hooks/accessControl/useCan/index.spec.tsx展示了renderHook与waitFor的组合用法import { renderHook, waitFor } from testing-library/react; import { vi } from vitest; import { TestWrapper, queryClient } from test; import { useCan } from .; describe(useCan Hook, () { it(can should return the true, async () { const { result } renderHook( () useCan({ action: list, resource: posts, params: { id: 1 } }), { wrapper: TestWrapper({ accessControlProvider: { can: ({ resource, action, params }) { if (action list resource posts params?.id 1) { return Promise.resolve({ can: true, reason: Access granted }); } return Promise.resolve({ can: false }); }, }, }), }, ); await waitFor(() { expect(result.current?.isFetched).toBeTruthy(); }); expect(result.current?.data?.can).toBeTruthy(); expect(result.current?.data?.reason).toBe(Access granted); }); // ... });这里把useCan包在TestWrapper中渲染TestWrapper负责提供 Refine 的RefineContext、DataProvider、queryClient等依赖——这正是测试 Context 组件章节思路的框架级体现。由于can是异步 Promise测试用waitFor等待isFetched变真后再断言data.can与data.reason。测试异步操作Mock fetch 与 waitFor异步操作涉及 Promise、异步函数与 async/await 语法。React 组件中最常见的异步操作是 HTTP 数据获取——它是非阻塞的与主代码并行执行。下面这个组件在挂载时请求数据并渲染结果import React, { useState, useEffect } from react; const AsyncComponent () { const [data, setData] useState(null); useEffect(() { const fetchData async () { const response await fetch(https://api.example.com/data); const result await response.json(); setData(result); }; fetchData(); }, []); return div{data ? data.message : Loading...}/div; }; export default AsyncComponent;测试时我们不希望发起真实 HTTP 请求因此要 mockfetch。同时fetch是异步操作需要用 React Testing Library 的waitFor它等待异步操作完成后再继续执行回调中的断言。import React from react; import { render, waitFor } from testing-library/react; import AsyncComponent from ./AsyncComponent; test(renders fetched data after async call, async () { const mockData { message: Test Message }; // Mock the fetch API jest.spyOn(window, fetch).mockImplementation(() Promise.resolve({ json: () Promise.resolve(mockData), }), ); const { getByText } render(AsyncComponent /); // Assert that Loading... is initially rendered expect(getByText(Loading...)).toBeInTheDocument(); // Wait for the async operation to complete await waitFor(() { expect(getByText(mockData.message)).toBeInTheDocument(); }); // Restore the original fetch implementation window.fetch.mockRestore(); });本用例的关键步骤定义mockData模拟 API 返回的数据用jest.spyOn(window, fetch).mockImplementation(...)mockfetch返回一个 resolved Promise其json()又 resolve 为mockDatarender(AsyncComponent /)渲染组件初始渲染显示Loading...用expectgetByText验证waitFor中轮询断言mockData.message已渲染到组件中最后调用window.fetch.mockRestore()恢复原始fetch实现避免污染其他测试。通过await与waitFor的组合我们可以可靠地断言异步操作的最终结果。补充现代 RTL 测试中findBy*查询如findByText本质上是getBy*与waitFor的语法糖也可用于异步断言。测试 React Context APIContext 测试的核心是验证组件是否正确消费/提供 Context 的值。假设有一个ThemeContext向下传递主题值import React, { createContext, useContext } from react; const ThemeContext createContext(); export const ThemeProvider ({ children }) { const theme light; return ( ThemeContext.Provider value{theme}{children}/ThemeContext.Provider ); }; export const useTheme () useContext(ThemeContext);ThemeProvider将 children 包裹在自己的标签之间并下发主题值useTheme通过useContext消费ThemeContext的当前值。测试一个用useTheme消费 Context 的组件import React from react; import { render } from testing-library/react; import { ThemeProvider, useTheme } from ./ThemeContext; const ThemeConsumer () { const theme useTheme(); return div{theme}/div; }; test(renders theme value from the context, () { const { getByText } render( ThemeProvider ThemeConsumer / /ThemeProvider, ); expect(getByText(light)).toBeInTheDocument(); });ThemeConsumer被ThemeProvider包裹从而获得主题值并渲染随后用getByText查询 DOM断言值为light的文本节点存在。这一用 Provider 包裹被测组件的模式正是 Refine 仓库中TestWrapper的工作原理——只是它的 Provider 层级更多RefineContext、QueryClientProvider、路由等。测试 React Router路由测试非常直观加载特定 URL然后验证对应组件被渲染到 DOM。假设应用有/home与/about两条路由分别映射到Home与About组件// Home.js const Home () { return divThis is the Home component/div; }; export default Home; // About.js const About () { return divThis is the About component/div; }; export default About;设置路由此处基于 react-router-dom v5 的 API 演示v6 中Switch已更名为Routesimport React from react; import { BrowserRouter as Router, Route, Link, Switch } from react-router-dom; import Home from ./Home; import About from ./About; const App () { return ( Router nav ul li Link to/homeHome/Link /li li Link to/aboutAbout/Link /li /ul /nav Switch Route exact path/home component{Home} / Route path/about component{About} / /Switch /Router ); }; export default App;测试用例用MemoryRouter这里写作Router并传initialEntries实践中应使用MemoryRouter指定初始 URL再断言对应组件文本出现import React from react; import { render, screen } from testing-library/react; import { MemoryRouter as Router } from react-router-dom; import App from ./App; test(renders home component when visiting the home route, () { render( Router initialEntries{[/home]} App / /Router, ); expect(screen.getByText(This is the Home component)).toBeInTheDocument(); }); test(renders about component when visiting the about route, () { render( Router initialEntries{[/about]} App / /Router, ); expect(screen.getByText(This is the About component)).toBeInTheDocument(); });技巧在于把App包裹在Router实践推荐MemoryRouter中并传入initialEntries{[/home]}。根据路由映射此时应渲染Home组件其文本This is the Home component出现在 DOM 中expect断言成立/about同理。测试验证了组件基于路由配置被正确渲染。补充Refine 作为数据驱动的 React 框架其核心包为不同路由框架react-router、nextjs-router、remix-router 等提供了统一的路由接口见 packages/react-router、packages/nextjs-router、packages/remix-router业务代码可以通过useGo、useParse等 hooks 解耦具体路由实现这让路由相关的单元测试可以针对统一的抽象层编写。快照测试Snapshot Testing快照测试属于输出对比型测试首次运行时会保存组件 UI 的快照后续运行时拿当前快照与历史快照对比检查是否有会导致破坏的变更。import React from react; const Button ({ text, onClick }) { return ( button onClick{onClick} classNamebutton {text} /button ); }; export default Button;使用toMatchSnapshot匹配器创建快照测试import React from react; import { render } from testing-library/react; import Button from ./Button; test(Button component matches snapshot, () { const { asFragment } render(Button textClick me onClick{() {}} /); expect(asFragment()).toMatchSnapshot(); });render渲染带 Props 的ButtonasFragment将渲染结果作为快照取回toMatchSnapshot与已保存快照对比无快照则 Jest 新建快照一致则测试通过有差异则 Jest 高亮差异并让测试失败。首次运行时Jest 会生成快照文件如Button.test.js.snap之后每次运行都对比。若组件输出被有意修改可用更新标志刷新快照jest --updateSnapshot快照测试非常适合防止意外回归但注意不要无脑接受快照更新——每次--updateSnapshot前都应人工确认 diff 是否符合预期。用 React Profiler 测试性能性能测试帮助确保组件不仅正确而且高效。用 Profiler 组件做基准测量Profiler是 React 内置组件可测量组件渲染性能。给MyComponent包上Profiler并传入onRender回调import React, { Profiler } from react; import { render } from testing-library/react; import MyComponent from ./MyComponent; const onRenderCallback ( id, // Profiler 树中刚刚提交的 id prop phase, // mount首次挂载或 update重渲染 actualDuration,// 本次提交实际渲染耗时 baseDuration, // 不使用 memoization 渲染整棵子树的大致耗时 startTime, // React 开始本次渲染的时间 commitTime, // React 提交本次更新的时间 interactions, // 本次更新所属的 interaction 集合 ) { console.log({ id, phase, actualDuration, baseDuration, startTime, commitTime, interactions }); }; test(measures performance of MyComponent, () { render( Profiler idMyComponent onRender{onRenderCallback} MyComponent / /Profiler, ); });onRenderCallback会输出各项渲染指标帮助我们定位性能瓶颈。借助性能工具React DevTools Profiler在真实运行时录制并分析组件性能帮助定位过度重渲染或渲染耗时过长的组件Lighthouse开源自动化工具用于提升网页质量提供全套性能指标与改进建议。可访问性测试Accessibility Testing用 Axe 自动检测可访问性问题最终目标应当是每个组件都尽可能无障碍为包括残障人士在内的所有用户提供良好体验。Axe 是运行在网页与 HTML 界面上的可访问性测试引擎配合jest-axe可在测试中自动发现并修复可访问性问题import React from react; import { render } from testing-library/react; import { axe, toHaveNoViolations } from jest-axe; expect.extend(toHaveNoViolations); const MyComponent () ( div h1Hello, World!/h1 buttonClick Me/button /div ); test(should have no accessibility violations, async () { const { container } render(MyComponent /); const results await axe(container); expect(results).toHaveNoViolations(); });axe(container)扫描组件 DOMtoHaveNoViolations匹配器断言组件不存在可访问性违规。类似地也可以参考 documentation/plugins/thumbs-up-down-feedback-widget 与 documentation/test/thumbs-up-down-feedback-widget/thumbs-up-down-feedback-widget.test.ts——仓库文档站用 Jest 对插件行为做了输入 AST、断言输出 AST的纯逻辑测试说明测试策略可以覆盖从 UI 到纯函数的各个层次。测试 ARIA Roles 与属性ARIAAccessible Rich Internet Applications角色与属性让网页内容对残障人士更易访问。测试组件是否正确使用了 ARIA 属性import React from react; import { render } from testing-library/react; import testing-library/jest-dom/extend-expect; const AriaComponent () ( div button aria-labelCloseX/button /div ); test(button should have correct aria-label, () { const { getByLabelText } render(AriaComponent /); const button getByLabelText(Close); expect(button).toBeInTheDocument(); });这里给按钮设置了aria-labelClose使其具有可访问的名称然后用getByLabelText按 ARIA 标签查询按钮并断言其存在。键盘导航测试模拟键盘事件并验证焦点管理确保用户能仅用键盘操作应用import React from react; import { render, fireEvent } from testing-library/react; import testing-library/jest-dom/extend-expect; const KeyboardComponent () ( div buttonFirst Button/button buttonSecond Button/button /div ); test(should navigate buttons using keyboard, () { const { getByText } render(KeyboardComponent /); const firstButton getByText(First Button); const secondButton getByText(Second Button); firstButton.focus(); expect(firstButton).toHaveFocus(); fireEvent.keyDown(document, { key: Tab }); expect(secondButton).toHaveFocus(); });测试模拟按下 Tab 键焦点从第一个按钮移动到第二个按钮fireEvent.keyDown(document, { key: Tab })模拟键盘事件toHaveFocus匹配器验证焦点管理是否正确该匹配器同样来自testing-library/jest-dom即 Refine 文档站在 documentation/test/jest.setup.ts 中全局引入的能力。结语本文完整覆盖了 React 单元测试的核心知识体系我们从单元测试的价值讲起完成了 Jest React Testing Library 的环境搭建随后逐一实践了基础组件渲染测试render、getByText、getByTestId、toBeInTheDocument、事件触发fireEvent.click、状态与 Props 断言进阶部分涵盖 Mock 函数jest.fn、自定义 HooksrenderHookact、异步操作mockfetchwaitFor、Context 组件Provider 包裹模式、路由测试MemoryRouterinitialEntries与快照测试toMatchSnapshot--updateSnapshot最后延伸到性能React Profiler与可访问性测试jest-axe、ARIA、键盘导航。在真实开源项目中这套方法论已被广泛实践Refine 文档站以 documentation/jest.config.js jsdom环境运行组件测试核心包 hooks 则通过renderHook、waitFor、act与TestWrapper组合验证useCan、useForgotPassword等关键能力见 packages/core/src/hooks/accessControl/useCan/index.spec.tsx、packages/core/src/hooks/auth/useForgotPassword/index.spec.ts。参照这些配置与用例你完全可以为任意 React 项目搭建同样严谨的测试体系——从组件到 Hooks再到 Context 与可访问性层层设防让每次重构都有底气。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考