创见博客
如何测试一个自定义hooks
七崽爱吃小饼干2026/01/19阅读 0专栏 React

如何测试 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 个包,一个是核心测试库,一个是测试运行器,直接执行:

bash
# 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 调用逻辑,它会帮我们:

  1. 创建合法的 React 执行上下文
  2. 在该上下文中执行你的自定义 Hooks
  3. 返回一个「Hook 执行结果对象」,包含 Hooks 的返回值、更新方法等

基础语法

js
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,包含 数字、加、减 功能

js
// 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 }
}

对应的测试代码(完整可运行):

js
// 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 补充测试,验证「加/减」功能:

js
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,传入一个数字,返回它的两倍值:

js
// 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
}

对应的测试代码(测试入参变化逻辑):

js
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)

js
// 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 }
}

对应的异步测试代码(完整最佳实践):

js
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 官方已经测过了,我们只需要测试「自己封装的业务逻辑」。


总结(核心知识点速记)

  1. 自定义 Hook 不能直接调用测试,会报 React 钩子调用错误,因为缺少合法执行上下文;
  2. 标准方案:@testing-library/react-hooks + Jest,必装依赖:@testing-library/react-hooks + react-test-renderer;
  3. 核心API:renderHook(() => 自定义Hook()) → 包裹Hook调用,通过 result.current 获取返回值;
  4. 状态更新必用:act() 包裹所有触发状态更新的操作;
  5. 入参变化必用:rerender() 手动触发Hook重新执行,传入新入参;
  6. 异步逻辑必用:await waitForNextUpdate() 等待异步状态更新完成;
  7. 黄金原则:测行为不测实现,断言返回值和状态变化即可。
评论
0/100