如何测试 React 自定义 Hooks
React 官方推荐、也是业界统一的标准方案 → 使用 @testing-library/react-hooks 这个库来测试自定义 Hooks,同时搭配 Jest 作为测试运行器,这是最优组合。
一、核心前置说明
为什么不能直接调用自定义 Hooks 测试?
自定义 Hooks 本质是以 use 开头的函数,但它有一个强制约束:必须遵循 React 的 Hooks 规则,只能在 React 的「函数组件/自定义 Hooks」的顶层调用。
如果你直接在普通的 Jest 测试函数中调用自定义 Hooks(比如 const result = useMyHook()),一定会报 React 经典错误:
Invalid hook call. Hooks can only be called inside of the body of a function component.
这是因为 Hooks 运行需要依赖 React 的「组件执行上下文」和「Hook 调用链表」,普通 JS 函数中没有这个环境,所以直接调用必然失败。
二、核心解决方案:@testing-library/react-hooks
2.1 核心作用
这个库的核心价值:为你的测试代码,模拟出一个合法的 React 组件执行上下文,让自定义 Hooks 可以在测试环境中被正常调用,同时还提供了一系列专门为 Hooks 设计的测试工具方法,完美契合 Hooks 的特性。
2.2 安装依赖(2个必装)
测试自定义 Hooks 只需要安装 2 个包,一个是核心测试库,一个是测试运行器,直接执行:
# npm 安装
npm install -D @testing-library/react-hooks react-test-renderer
# yarn 安装
yarn add -D @testing-library/react-hooks react-test-renderer
依赖说明:
react-test-renderer是@testing-library/react-hooks的底层依赖,用来模拟 React 的组件渲染,必须一起装。
三、核心API:renderHook
@testing-library/react-hooks 提供的最核心、最常用的 API 就是 renderHook,所有自定义 Hooks 的测试,几乎都是基于它实现的。
核心作用
renderHook(cb) 接收一个回调函数,回调函数内部书写要测试的自定义 Hooks 调用逻辑,它会帮我们:
- 创建合法的 React 执行上下文
- 在该上下文中执行你的自定义 Hooks
- 返回一个「Hook 执行结果对象」,包含 Hooks 的返回值、更新方法等
基础语法
import { renderHook } from '@testing-library/react-hooks'
// 导入你要测试的自定义Hook
import { useMyCustomHook } from './hooks'
test('测试自定义Hook的基础功能', () => {
// 核心:用renderHook包裹自定义Hook的调用
const { result } = renderHook(() => useMyCustomHook())
// result.current 就是 自定义Hook的返回值(重中之重)
console.log(result.current) // 拿到Hook的返回内容,做断言测试
})
四、完整实战示例
场景1:测试「无依赖、纯逻辑」的自定义Hook(基础)
先写一个简单的自定义 Hook:useCount 计数器 Hook,包含 数字、加、减 功能
// src/hooks/useCount.js
import { useState } from 'react'
export function useCount(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = () => setCount(prev => prev + 1)
const decrement = () => setCount(prev => prev - 1)
return { count, increment, decrement }
}
对应的测试代码(完整可运行):
// src/hooks/__tests__/useCount.test.js
import { renderHook } from '@testing-library/react-hooks'
import { useCount } from '../useCount'
test('useCount 初始化值为0', () => {
const { result } = renderHook(() => useCount())
// 断言:初始count值等于0
expect(result.current.count).toBe(0)
})
test('useCount 支持传入自定义初始值', () => {
const { result } = renderHook(() => useCount(10))
expect(result.current.count).toBe(10)
})
五、关键场景:测试「Hook内部状态更新」的逻辑
自定义 Hooks 大多会封装 useState/useReducer 状态,核心测试点就是:调用Hook暴露的方法后,状态是否正确更新。
核心API补充:act() 函数
React 中所有会触发「状态更新/组件重渲染」的操作,都必须被 act() 包裹,否则会报警告,且状态更新不会被正确捕获。
@testing-library/react-hooks 会自动导出 React 官方的 act,也可以从 react-test-renderer 导入,二选一即可。
完整测试
给上面的 useCount 补充测试,验证「加/减」功能:
import { renderHook, act } from '@testing-library/react-hooks'
import { useCount } from '../useCount'
test('调用increment,count会加1', () => {
const { result } = renderHook(() => useCount())
// ✅ 核心:触发状态更新的操作,必须用act包裹
act(() => {
result.current.increment()
})
// 断言:更新后的count值为1
expect(result.current.count).toBe(1)
})
test('调用decrement,count会减1', () => {
const { result } = renderHook(() => useCount(5))
act(() => {
result.current.decrement()
})
expect(result.current.count).toBe(4)
})
六、进阶场景:测试「带依赖/入参变化」的自定义Hook
很多自定义 Hook 会根据入参/依赖项变化重新执行逻辑(比如监听props变化、传入配置项变化),这种场景用 renderHook 的 rerender() 方法完美解决。
核心API补充:rerender() 函数
renderHook 的返回值中,除了 result,还有一个 rerender() 方法,作用是:手动触发Hook的重新执行,并且可以传入新的入参。
实战示例
比如我们有一个根据入参计算的 Hook:useDouble,传入一个数字,返回它的两倍值:
// src/hooks/useDouble.js
import { useState, useEffect } from 'react'
export function useDouble(num) {
const [doubleNum, setDoubleNum] = useState(num * 2)
useEffect(() => {
setDoubleNum(num * 2)
}, [num]) // 依赖num变化,重新计算
return doubleNum
}
对应的测试代码(测试入参变化逻辑):
import { renderHook } from '@testing-library/react-hooks'
import { useDouble } from '../useDouble'
test('useDouble 入参变化时,返回值同步更新', () => {
// 1. 初始化调用,传入入参 10
const { result, rerender } = renderHook(({ num }) => useDouble(num), {
initialProps: { num: 10 } // 初始化入参
})
// 初始断言:10*2=20
expect(result.current).toBe(20)
// 2. 调用rerender,传入新的入参 20 → 触发Hook重新执行
rerender({ num: 20 })
expect(result.current).toBe(40)
// 3. 再次rerender,传入新的入参 0
rerender({ num: 0 })
expect(result.current).toBe(0)
})
七、补充:测试「异步逻辑」的自定义Hook
自定义 Hooks 经常会封装异步逻辑(比如 useEffect 中发请求、异步获取数据),这种场景是重点,也是面试高频考点。
核心方案:waitForNextUpdate() + async/await
renderHook 提供了 waitForNextUpdate() 方法,返回一个 Promise,作用是:等待 Hook 内部的下一次状态更新完成(比如异步请求成功后 setState),完美适配异步场景。
实战示例(封装异步请求的Hook)
// src/hooks/useUser.js
import { useState, useEffect } from 'react'
export function useUser(userId) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchUser = async () => {
setLoading(true)
// 模拟异步请求接口
const res = await fetch(`/api/user/${userId}`)
const data = await res.json()
setUser(data)
setLoading(false)
}
fetchUser()
}, [userId])
return { user, loading }
}
对应的异步测试代码(完整最佳实践):
import { renderHook, waitForNextUpdate } from '@testing-library/react-hooks'
import { useUser } from '../useUser'
// 模拟fetch请求,避免真实发请求
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: '张三' })
})
)
test('useUser 异步获取用户数据', async () => {
const { result } = renderHook(() => useUser(1))
// 1. 初始状态:loading为true,user为null
expect(result.current.loading).toBe(true)
expect(result.current.user).toBe(null)
// 2. 等待异步请求完成 → 状态更新
await waitForNextUpdate()
// 3. 异步完成后断言
expect(result.current.loading).toBe(false)
expect(result.current.user).toEqual({ id: 1, name: '张三' })
})
八、完整测试原则 & 最佳实践(必看)
1. 测试原则:只测「外部行为」,不测「内部实现」
✅ 推荐:断言 Hook 的返回值、状态变化、入参出参映射关系
❌ 禁止:断言 Hook 内部的局部变量、是否调用了 useState/useEffect、执行了多少次 setState
比如:你测试 useCount,只需要断言「调用 increment 后 count+1」,而不要去测试「Hook 内部是否调用了 useState」。
→ 原因:自定义 Hook 的内部实现可能随时重构(比如 useState 改 useReducer),但外部行为不变,测试用例不需要修改,这是单元测试的核心原则。
2. 测试范围:覆盖「核心场景 + 边界场景」
- 核心场景:初始化值、入参映射、状态更新、异步结果
- 边界场景:入参为0/空/undefined、异步失败、依赖项为null等
3. 必做:mock 外部依赖
如果 Hook 依赖了外部接口(fetch/axios)、第三方库、浏览器API(localStorage),一定要用 Jest 模拟(mock),避免真实调用,保证测试的「独立性、稳定性、速度」。
4. 不要过度测试
不需要测试 React 自身的 Hooks 功能(比如 useState 是否能正常更新),React 官方已经测过了,我们只需要测试「自己封装的业务逻辑」。
总结(核心知识点速记)
- 自定义 Hook 不能直接调用测试,会报 React 钩子调用错误,因为缺少合法执行上下文;
- 标准方案:
@testing-library/react-hooks+Jest,必装依赖:@testing-library/react-hooks+react-test-renderer; - 核心API:
renderHook(() => 自定义Hook())→ 包裹Hook调用,通过result.current获取返回值; - 状态更新必用:
act()包裹所有触发状态更新的操作; - 入参变化必用:
rerender()手动触发Hook重新执行,传入新入参; - 异步逻辑必用:
await waitForNextUpdate()等待异步状态更新完成; - 黄金原则:测行为不测实现,断言返回值和状态变化即可。